From 66ef217647e14ab8f72c8ba6573267997b05d103 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Thu, 25 Nov 2021 11:22:08 +0100 Subject: [PATCH 01/29] add perimeter geometry filter --- .../ohsome/oshdb/filter/FilterParser.java | 7 +++++- .../oshdb/filter/GeometryFilterPerimeter.java | 20 ++++++++++++++++ .../oshdb/filter/ApplyOSMGeometryTest.java | 23 +++++++++++++++++++ .../heigit/ohsome/oshdb/filter/ParseTest.java | 6 +++++ 4 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterPerimeter.java diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java index df0875107..c9e17e3ee 100644 --- a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java @@ -96,6 +96,7 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) { .map(ignored -> "*"); final Parser area = Patterns.string("area").toScanner("area"); final Parser length = Patterns.string("length").toScanner("length"); + final Parser perimeter = Patterns.string("perimeter").toScanner("perimeter"); final Parser changeset = Patterns.string("changeset").toScanner("changeset"); final Parser contributor = Patterns.string("contributor").toScanner("contributor"); @@ -212,9 +213,13 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) { final Parser geometryFilterLength = Parsers.sequence( length, colon, floatingRange ).map(GeometryFilterLength::new); + final Parser geometryFilterPerimeter = Parsers.sequence( + perimeter, colon, floatingRange + ).map(GeometryFilterPerimeter::new); final Parser geometryFilter = Parsers.or( geometryFilterArea, - geometryFilterLength); + geometryFilterLength, + geometryFilterPerimeter); // changeset id filters final Parser changesetIdFilter = Parsers.sequence( diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterPerimeter.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterPerimeter.java new file mode 100644 index 000000000..b87a28315 --- /dev/null +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterPerimeter.java @@ -0,0 +1,20 @@ +package org.heigit.ohsome.oshdb.filter; + +import javax.annotation.Nonnull; +import org.heigit.ohsome.oshdb.util.geometry.Geo; +import org.locationtech.jts.geom.Polygonal; + +/** + * A filter which checks the perimeter of polygonal OSM feature geometries. + */ +public class GeometryFilterPerimeter extends GeometryFilter { + public GeometryFilterPerimeter(@Nonnull ValueRange range) { + super(range, GeometryMetricEvaluator.fromLambda(geometry -> { + if (!(geometry instanceof Polygonal)) { + return 0; + } + var boundary = geometry.getBoundary(); + return Geo.lengthOf(boundary); + }, "perimeter")); + } +} diff --git a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java index 81441eb3f..7c6f89f2e 100644 --- a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java +++ b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java @@ -161,4 +161,27 @@ public void testGeometryFilterLength() { }) )); } + + @Test + public void testGeometryFilterPerimeter() { + FilterExpression expression = parser.parse("perimeter:(4..5)"); + OSMEntity entity = createTestOSMEntityWay(new long[] {1, 2, 3, 4, 1}); + assertFalse(expression.applyOSMGeometry(entity, + // approx 4 x 0.6m + OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 5E-6, 5E-6)) + )); + assertTrue(expression.applyOSMGeometry(entity, + // approx 4 x 1.1m + OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 1E-5, 1E-5)) + )); + assertFalse(expression.applyOSMGeometry(entity, + // approx 4 x 2.2m + OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 2E-5, 2E-5)) + )); + // negated + assertTrue(expression.negate().applyOSMGeometry(entity, + // approx 4 x 0.6m + OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 5E-6, 5E-6)) + )); + } } diff --git a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ParseTest.java b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ParseTest.java index 4a33a5186..663b7d353 100644 --- a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ParseTest.java +++ b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ParseTest.java @@ -329,6 +329,12 @@ public void testGeometryFilterLength() { assertTrue(expression instanceof GeometryFilterLength); } + @Test + public void testGeometryFilterPerimeter() { + FilterExpression expression = parser.parse("perimeter:(1..10)"); + assertTrue(expression instanceof GeometryFilterPerimeter); + } + @Test public void testChangesetIdFilter() { FilterExpression expression = parser.parse("changeset:42"); From c9a3d082863605f507b92da244aaf959b25dc6c7 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Thu, 25 Nov 2021 12:32:39 +0100 Subject: [PATCH 02/29] add vertices "numPoints" filter --- .../ohsome/oshdb/filter/FilterParser.java | 19 ++++- .../oshdb/filter/GeometryFilterVertices.java | 13 +++ .../oshdb/filter/ApplyOSMGeometryTest.java | 79 +++++++++++++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterVertices.java diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java index c9e17e3ee..ebda11530 100644 --- a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java @@ -97,6 +97,7 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) { final Parser area = Patterns.string("area").toScanner("area"); final Parser length = Patterns.string("length").toScanner("length"); final Parser perimeter = Patterns.string("perimeter").toScanner("perimeter"); + final Parser vertices = Patterns.string("vertices").toScanner("vertices"); final Parser changeset = Patterns.string("changeset").toScanner("changeset"); final Parser contributor = Patterns.string("contributor").toScanner("contributor"); @@ -207,6 +208,18 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) { ), Scanners.isChar(')') ); + final Parser decimalRange = Parsers.between( + Scanners.isChar('('), + Parsers.or( + Parsers.sequence(number, dotdot, number, + (min, ignored, max) -> new ValueRange(min, max)), + number.followedBy(dotdot).map( + min -> new ValueRange(min, Double.POSITIVE_INFINITY)), + Parsers.sequence(dotdot, number).map( + max -> new ValueRange(Double.NEGATIVE_INFINITY, max)) + ), + Scanners.isChar(')') + ); final Parser geometryFilterArea = Parsers.sequence( area, colon, floatingRange ).map(GeometryFilterArea::new); @@ -216,10 +229,14 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) { final Parser geometryFilterPerimeter = Parsers.sequence( perimeter, colon, floatingRange ).map(GeometryFilterPerimeter::new); + final Parser geometryFilterVertices = Parsers.sequence( + vertices, colon, decimalRange + ).map(GeometryFilterVertices::new); final Parser geometryFilter = Parsers.or( geometryFilterArea, geometryFilterLength, - geometryFilterPerimeter); + geometryFilterPerimeter, + geometryFilterVertices); // changeset id filters final Parser changesetIdFilter = Parsers.sequence( diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterVertices.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterVertices.java new file mode 100644 index 000000000..7d324a20d --- /dev/null +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterVertices.java @@ -0,0 +1,13 @@ +package org.heigit.ohsome.oshdb.filter; + +import javax.annotation.Nonnull; +import org.locationtech.jts.geom.Geometry; + +/** + * A filter which checks the number of vertices of OSM feature geometries. + */ +public class GeometryFilterVertices extends GeometryFilter { + public GeometryFilterVertices(@Nonnull ValueRange range) { + super(range, GeometryMetricEvaluator.fromLambda(Geometry::getNumPoints, "vertices")); + } +} diff --git a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java index 7c6f89f2e..175cc80c9 100644 --- a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java +++ b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java @@ -3,12 +3,20 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import com.google.common.collect.Streams; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.stream.LongStream; +import java.util.stream.Stream; import org.heigit.ohsome.oshdb.OSHDBBoundingBox; import org.heigit.ohsome.oshdb.osm.OSMEntity; import org.heigit.ohsome.oshdb.util.geometry.OSHDBGeometryBuilder; +import org.junit.Assert; import org.junit.Test; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.Polygon; /** * Tests the application of filters to OSM geometries. @@ -184,4 +192,75 @@ public void testGeometryFilterPerimeter() { OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 5E-6, 5E-6)) )); } + + @Test + public void testGeometryFilterVertices() { + FilterExpression expression = parser.parse("vertices:(11..13)"); + // lines + BiConsumer> testLineN = (n, tester) -> { + var entity = createTestOSMEntityWay(LongStream.rangeClosed(1, n).toArray()); + var coords = LongStream.rangeClosed(1, n) + .mapToObj(i -> new Coordinate(i, i)).toArray(Coordinate[]::new); + var geometry = gf.createLineString(coords); + tester.accept(expression.applyOSMGeometry(entity, geometry)); + }; + testLineN.accept(10, Assert::assertFalse); + testLineN.accept(11, Assert::assertTrue); + testLineN.accept(12, Assert::assertTrue); + testLineN.accept(13, Assert::assertTrue); + testLineN.accept(14, Assert::assertFalse); + // polygons + BiConsumer> testPolyonN = (n, tester) -> { + var entity = createTestOSMEntityWay(LongStream.rangeClosed(1, n).toArray()); + var coords = Streams.concat( + LongStream.rangeClosed(1, n - 1).mapToObj(i -> new Coordinate(i, Math.pow(i, 2))), + Stream.of(new Coordinate(1, 1)) + ).toArray(Coordinate[]::new); + var geometry = gf.createPolygon(coords); + tester.accept(expression.applyOSMGeometry(entity, geometry)); + }; + testPolyonN.accept(10, Assert::assertFalse); + testPolyonN.accept(11, Assert::assertTrue); + testPolyonN.accept(12, Assert::assertTrue); + testPolyonN.accept(13, Assert::assertTrue); + testPolyonN.accept(14, Assert::assertFalse); + // polygon with hole + BiConsumer> testPolyonWithHoleN = (n, tester) -> { + var entity = createTestOSMEntityRelation("type", "multipolygon"); + n -= 5; // outer shell is a simple bbox with 5 points + var innerCoords = gf.createLinearRing(Streams.concat( + LongStream.rangeClosed(1, n - 1).mapToObj(i -> new Coordinate(i, Math.pow(i, 2))), + Stream.of(new Coordinate(1, 1)) + ).toArray(Coordinate[]::new)); + var geometry = gf.createPolygon( + OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox + .bboxWgs84Coordinates(-80, -80, 80, 80)).getExteriorRing(), + new LinearRing[] { innerCoords }); + tester.accept(expression.applyOSMGeometry(entity, geometry)); + }; + testPolyonWithHoleN.accept(10, Assert::assertFalse); + testPolyonWithHoleN.accept(11, Assert::assertTrue); + testPolyonWithHoleN.accept(12, Assert::assertTrue); + testPolyonWithHoleN.accept(13, Assert::assertTrue); + testPolyonWithHoleN.accept(14, Assert::assertFalse); + // multi polygon + BiConsumer> testMultiPolyonN = (n, tester) -> { + var entity = createTestOSMEntityRelation("type", "multipolygon"); + n -= 5; // outer shell 2 is a simple bbox with 5 points + var coords = Streams.concat( + LongStream.rangeClosed(1, n - 1).mapToObj(i -> new Coordinate(i, Math.pow(i, 2))), + Stream.of(new Coordinate(1, 1)) + ).toArray(Coordinate[]::new); + var geometry = gf.createMultiPolygon(new Polygon[] { + OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(-2, -2, -1, -1)), + gf.createPolygon(coords) }); + tester.accept(expression.applyOSMGeometry(entity, geometry)); + }; + testMultiPolyonN.accept(10, Assert::assertFalse); + testMultiPolyonN.accept(11, Assert::assertTrue); + testMultiPolyonN.accept(12, Assert::assertTrue); + testMultiPolyonN.accept(13, Assert::assertTrue); + testMultiPolyonN.accept(14, Assert::assertFalse); + + } } From aaccc30768372c59036c5fb254a3d20e3ffd0c88 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Thu, 25 Nov 2021 13:56:49 +0100 Subject: [PATCH 03/29] add outers/inners filter + slight adjustment to vertices filter --- .../ohsome/oshdb/filter/FilterParser.java | 18 ++++-- .../filter/GeometryFilterInnerRings.java | 31 ++++++++++ .../filter/GeometryFilterOuterRings.java | 20 +++++++ .../oshdb/filter/GeometryFilterPerimeter.java | 5 ++ .../ohsome/oshdb/filter/NegatableFilter.java | 4 +- .../oshdb/filter/ApplyOSMGeometryTest.java | 59 +++++++++++++++++++ .../heigit/ohsome/oshdb/filter/ParseTest.java | 22 +++++++ 7 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterInnerRings.java create mode 100644 oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterOuterRings.java diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java index ebda11530..2ebc5c658 100644 --- a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java @@ -98,6 +98,8 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) { final Parser length = Patterns.string("length").toScanner("length"); final Parser perimeter = Patterns.string("perimeter").toScanner("perimeter"); final Parser vertices = Patterns.string("vertices").toScanner("vertices"); + final Parser outers = Patterns.string("outers").toScanner("outers"); + final Parser inners = Patterns.string("inners").toScanner("inners"); final Parser changeset = Patterns.string("changeset").toScanner("changeset"); final Parser contributor = Patterns.string("contributor").toScanner("contributor"); @@ -208,7 +210,7 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) { ), Scanners.isChar(')') ); - final Parser decimalRange = Parsers.between( + final Parser positiveIntegerRange = Parsers.between( Scanners.isChar('('), Parsers.or( Parsers.sequence(number, dotdot, number, @@ -216,7 +218,7 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) { number.followedBy(dotdot).map( min -> new ValueRange(min, Double.POSITIVE_INFINITY)), Parsers.sequence(dotdot, number).map( - max -> new ValueRange(Double.NEGATIVE_INFINITY, max)) + max -> new ValueRange(0, max)) ), Scanners.isChar(')') ); @@ -230,13 +232,21 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) { perimeter, colon, floatingRange ).map(GeometryFilterPerimeter::new); final Parser geometryFilterVertices = Parsers.sequence( - vertices, colon, decimalRange + vertices, colon, positiveIntegerRange ).map(GeometryFilterVertices::new); + final Parser geometryFilterOuters = Parsers.sequence( + outers, colon, Parsers.or(positiveIntegerRange, number.map(n -> new ValueRange(n, n))) + ).map(GeometryFilterOuterRings::new); + final Parser geometryFilterInners = Parsers.sequence( + inners, colon, Parsers.or(positiveIntegerRange, number.map(n -> new ValueRange(n, n))) + ).map(GeometryFilterInnerRings::new); final Parser geometryFilter = Parsers.or( geometryFilterArea, geometryFilterLength, geometryFilterPerimeter, - geometryFilterVertices); + geometryFilterVertices, + geometryFilterOuters, + geometryFilterInners); // changeset id filters final Parser changesetIdFilter = Parsers.sequence( diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterInnerRings.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterInnerRings.java new file mode 100644 index 000000000..c03741eff --- /dev/null +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterInnerRings.java @@ -0,0 +1,31 @@ +package org.heigit.ohsome.oshdb.filter; + +import javax.annotation.Nonnull; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Polygon; + +/** + * A filter which checks the number of inner rings of a multipolygon relation. + */ +public class GeometryFilterInnerRings extends GeometryFilter { + /** + * Creates a new inner rings filter object. + * + * @param range the allowed range (inclusive) of values to pass the filter + */ + public GeometryFilterInnerRings(@Nonnull ValueRange range) { + super(range, GeometryMetricEvaluator.fromLambda(geometry -> { + if (geometry instanceof Polygon) { + return ((Polygon) geometry).getNumInteriorRing(); + } else if (geometry instanceof MultiPolygon) { + var counter = 0; + for (var i = 0; i < geometry.getNumGeometries(); i++) { + counter += ((Polygon) geometry.getGeometryN(i)).getNumInteriorRing(); + } + return counter; + } else { + return -1; + } + }, "outers")); + } +} diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterOuterRings.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterOuterRings.java new file mode 100644 index 000000000..f53589463 --- /dev/null +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterOuterRings.java @@ -0,0 +1,20 @@ +package org.heigit.ohsome.oshdb.filter; + +import javax.annotation.Nonnull; +import org.locationtech.jts.geom.Polygonal; + +/** + * A filter which checks the number of outer rings of a multipolygon relation. + */ +public class GeometryFilterOuterRings extends GeometryFilter { + /** + * Creates a new outer rings filter object. + * + * @param range the allowed range (inclusive) of values to pass the filter + */ + public GeometryFilterOuterRings(@Nonnull ValueRange range) { + super(range, GeometryMetricEvaluator.fromLambda(geometry -> + geometry instanceof Polygonal ? geometry.getNumGeometries() : -1, + "outers")); + } +} diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterPerimeter.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterPerimeter.java index b87a28315..a109964de 100644 --- a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterPerimeter.java +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterPerimeter.java @@ -8,6 +8,11 @@ * A filter which checks the perimeter of polygonal OSM feature geometries. */ public class GeometryFilterPerimeter extends GeometryFilter { + /** + * Creates a new perimeter filter object. + * + * @param range the allowed range (inclusive) of values to pass the filter + */ public GeometryFilterPerimeter(@Nonnull ValueRange range) { super(range, GeometryMetricEvaluator.fromLambda(geometry -> { if (!(geometry instanceof Polygonal)) { diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/NegatableFilter.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/NegatableFilter.java index fc2d33a23..26bf54aaa 100644 --- a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/NegatableFilter.java +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/NegatableFilter.java @@ -21,7 +21,7 @@ public FilterExpression negate() { throw new IllegalStateException("Invalid call of inner negate() on a negatable filter"); } - /** Inverse of {@link FilterExpression#applyOSH(OSHEntity)} */ + /** Inverse of {@link FilterExpression#applyOSH(OSHEntity)}. */ @Contract(pure = true) boolean applyOSHNegated(OSHEntity entity) { return true; @@ -32,7 +32,7 @@ public boolean applyOSM(OSMEntity entity) { return true; } - /** Inverse of {@link FilterExpression#applyOSM(OSMEntity)} */ + /** Inverse of {@link FilterExpression#applyOSM(OSMEntity)}. */ @Contract(pure = true) boolean applyOSMNegated(OSMEntity entity) { return true; diff --git a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java index 175cc80c9..926274fd9 100644 --- a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java +++ b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java @@ -196,6 +196,10 @@ public void testGeometryFilterPerimeter() { @Test public void testGeometryFilterVertices() { FilterExpression expression = parser.parse("vertices:(11..13)"); + // point + assertFalse(expression.applyOSMGeometry( + createTestOSMEntityNode("natural", "tree"), + gf.createPoint(new Coordinate(0, 0)))); // lines BiConsumer> testLineN = (n, tester) -> { var entity = createTestOSMEntityWay(LongStream.rangeClosed(1, n).toArray()); @@ -261,6 +265,61 @@ public void testGeometryFilterVertices() { testMultiPolyonN.accept(12, Assert::assertTrue); testMultiPolyonN.accept(13, Assert::assertTrue); testMultiPolyonN.accept(14, Assert::assertFalse); + } + + @Test + public void testGeometryFilterOuters() { + FilterExpression expression = parser.parse("outers:1"); + OSMEntity entity = createTestOSMEntityRelation("type", "multipolygon"); + assertFalse(expression.applyOSMGeometry(entity, gf.createMultiPolygon(new Polygon[] { + OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(1, 1, 2, 2)), + OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(3, 3, 4, 4)) + }))); + assertTrue(expression.applyOSMGeometry(entity, gf.createMultiPolygon(new Polygon[] { + OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(1, 1, 2, 2)) + }))); + assertTrue(expression.applyOSMGeometry(entity, + OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(1, 1, 2, 2)) + )); + // range + expression = parser.parse("outers:(2..)"); + assertTrue(expression.applyOSMGeometry(entity, gf.createMultiPolygon(new Polygon[] { + OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(1, 1, 2, 2)), + OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(3, 3, 4, 4)) + }))); + } + @Test + public void testGeometryFilterInners() { + FilterExpression expression = parser.parse("inners:0"); + OSMEntity entity = createTestOSMEntityRelation("type", "multipolygon"); + assertTrue(expression.applyOSMGeometry(entity, + OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(1, 1, 2, 2)) + )); + assertFalse(expression.applyOSMGeometry(entity, gf.createPolygon( + OSHDBGeometryBuilder.getGeometry( + OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 10, 10)).getExteriorRing(), + new LinearRing[] { OSHDBGeometryBuilder.getGeometry( + OSHDBBoundingBox.bboxWgs84Coordinates(1, 1, 2, 2)).getExteriorRing() + }))); + assertTrue(expression.applyOSMGeometry(entity, gf.createMultiPolygon(new Polygon[] { + OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(1, 1, 2, 2)) + }))); + assertFalse(expression.applyOSMGeometry(entity, gf.createMultiPolygon(new Polygon[] { + gf.createPolygon( + OSHDBGeometryBuilder.getGeometry( + OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 10, 10)).getExteriorRing(), + new LinearRing[] { OSHDBGeometryBuilder.getGeometry( + OSHDBBoundingBox.bboxWgs84Coordinates(1, 1, 2, 2)).getExteriorRing() + }) + }))); + // range + expression = parser.parse("inners:(1..)"); + assertTrue(expression.applyOSMGeometry(entity, gf.createPolygon( + OSHDBGeometryBuilder.getGeometry( + OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 10, 10)).getExteriorRing(), + new LinearRing[] { OSHDBGeometryBuilder.getGeometry( + OSHDBBoundingBox.bboxWgs84Coordinates(1, 1, 2, 2)).getExteriorRing() + }))); } } diff --git a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ParseTest.java b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ParseTest.java index 663b7d353..29aad2289 100644 --- a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ParseTest.java +++ b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ParseTest.java @@ -335,6 +335,28 @@ public void testGeometryFilterPerimeter() { assertTrue(expression instanceof GeometryFilterPerimeter); } + @Test + public void testGeometryFilterVertices() { + FilterExpression expression = parser.parse("vertices:(1..10)"); + assertTrue(expression instanceof GeometryFilterVertices); + } + + @Test + public void testGeometryFilterOuters() { + FilterExpression expression = parser.parse("outers:2"); + assertTrue(expression instanceof GeometryFilterOuterRings); + expression = parser.parse("outers:(1..10)"); + assertTrue(expression instanceof GeometryFilterOuterRings); + } + + @Test + public void testGeometryFilterInners() { + FilterExpression expression = parser.parse("inners:0"); + assertTrue(expression instanceof GeometryFilterInnerRings); + expression = parser.parse("inners:(1..10)"); + assertTrue(expression instanceof GeometryFilterInnerRings); + } + @Test public void testChangesetIdFilter() { FilterExpression expression = parser.parse("changeset:42"); From df57a5b9223100eeb175c5391ff184e577992f18 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Thu, 9 Dec 2021 11:10:19 +0100 Subject: [PATCH 04/29] rename inners/outer/vertices filters to have a "geometry." prefix --- .../org/heigit/ohsome/oshdb/filter/FilterParser.java | 7 ++++--- .../ohsome/oshdb/filter/GeometryFilterInnerRings.java | 2 +- .../ohsome/oshdb/filter/GeometryFilterOuterRings.java | 2 +- .../ohsome/oshdb/filter/GeometryFilterVertices.java | 2 +- .../ohsome/oshdb/filter/ApplyOSMGeometryTest.java | 10 +++++----- .../java/org/heigit/ohsome/oshdb/filter/ParseTest.java | 10 +++++----- 6 files changed, 17 insertions(+), 16 deletions(-) diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java index 2ebc5c658..e864a3b1d 100644 --- a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java @@ -97,9 +97,10 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) { final Parser area = Patterns.string("area").toScanner("area"); final Parser length = Patterns.string("length").toScanner("length"); final Parser perimeter = Patterns.string("perimeter").toScanner("perimeter"); - final Parser vertices = Patterns.string("vertices").toScanner("vertices"); - final Parser outers = Patterns.string("outers").toScanner("outers"); - final Parser inners = Patterns.string("inners").toScanner("inners"); + final Parser vertices = Patterns.string("geometry.vertices") + .toScanner("geometry.vertices"); + final Parser outers = Patterns.string("geometry.outers").toScanner("geometry.outers"); + final Parser inners = Patterns.string("geometry.inners").toScanner("geometry.inners"); final Parser changeset = Patterns.string("changeset").toScanner("changeset"); final Parser contributor = Patterns.string("contributor").toScanner("contributor"); diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterInnerRings.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterInnerRings.java index c03741eff..e4cb64c7f 100644 --- a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterInnerRings.java +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterInnerRings.java @@ -26,6 +26,6 @@ public GeometryFilterInnerRings(@Nonnull ValueRange range) { } else { return -1; } - }, "outers")); + }, "geometry.outers")); } } diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterOuterRings.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterOuterRings.java index f53589463..1ab2d83dd 100644 --- a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterOuterRings.java +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterOuterRings.java @@ -15,6 +15,6 @@ public class GeometryFilterOuterRings extends GeometryFilter { public GeometryFilterOuterRings(@Nonnull ValueRange range) { super(range, GeometryMetricEvaluator.fromLambda(geometry -> geometry instanceof Polygonal ? geometry.getNumGeometries() : -1, - "outers")); + "geometry.outers")); } } diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterVertices.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterVertices.java index 7d324a20d..eed91e140 100644 --- a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterVertices.java +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterVertices.java @@ -8,6 +8,6 @@ */ public class GeometryFilterVertices extends GeometryFilter { public GeometryFilterVertices(@Nonnull ValueRange range) { - super(range, GeometryMetricEvaluator.fromLambda(Geometry::getNumPoints, "vertices")); + super(range, GeometryMetricEvaluator.fromLambda(Geometry::getNumPoints, "geometry.vertices")); } } diff --git a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java index 926274fd9..3b88eb2f8 100644 --- a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java +++ b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java @@ -195,7 +195,7 @@ public void testGeometryFilterPerimeter() { @Test public void testGeometryFilterVertices() { - FilterExpression expression = parser.parse("vertices:(11..13)"); + FilterExpression expression = parser.parse("geometry.vertices:(11..13)"); // point assertFalse(expression.applyOSMGeometry( createTestOSMEntityNode("natural", "tree"), @@ -269,7 +269,7 @@ public void testGeometryFilterVertices() { @Test public void testGeometryFilterOuters() { - FilterExpression expression = parser.parse("outers:1"); + FilterExpression expression = parser.parse("geometry.outers:1"); OSMEntity entity = createTestOSMEntityRelation("type", "multipolygon"); assertFalse(expression.applyOSMGeometry(entity, gf.createMultiPolygon(new Polygon[] { OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(1, 1, 2, 2)), @@ -282,7 +282,7 @@ public void testGeometryFilterOuters() { OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(1, 1, 2, 2)) )); // range - expression = parser.parse("outers:(2..)"); + expression = parser.parse("geometry.outers:(2..)"); assertTrue(expression.applyOSMGeometry(entity, gf.createMultiPolygon(new Polygon[] { OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(1, 1, 2, 2)), OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(3, 3, 4, 4)) @@ -291,7 +291,7 @@ public void testGeometryFilterOuters() { @Test public void testGeometryFilterInners() { - FilterExpression expression = parser.parse("inners:0"); + FilterExpression expression = parser.parse("geometry.inners:0"); OSMEntity entity = createTestOSMEntityRelation("type", "multipolygon"); assertTrue(expression.applyOSMGeometry(entity, OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(1, 1, 2, 2)) @@ -314,7 +314,7 @@ public void testGeometryFilterInners() { }) }))); // range - expression = parser.parse("inners:(1..)"); + expression = parser.parse("geometry.inners:(1..)"); assertTrue(expression.applyOSMGeometry(entity, gf.createPolygon( OSHDBGeometryBuilder.getGeometry( OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 10, 10)).getExteriorRing(), diff --git a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ParseTest.java b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ParseTest.java index 29aad2289..3c2fae0a6 100644 --- a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ParseTest.java +++ b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ParseTest.java @@ -337,23 +337,23 @@ public void testGeometryFilterPerimeter() { @Test public void testGeometryFilterVertices() { - FilterExpression expression = parser.parse("vertices:(1..10)"); + FilterExpression expression = parser.parse("geometry.vertices:(1..10)"); assertTrue(expression instanceof GeometryFilterVertices); } @Test public void testGeometryFilterOuters() { - FilterExpression expression = parser.parse("outers:2"); + FilterExpression expression = parser.parse("geometry.outers:2"); assertTrue(expression instanceof GeometryFilterOuterRings); - expression = parser.parse("outers:(1..10)"); + expression = parser.parse("geometry.outers:(1..10)"); assertTrue(expression instanceof GeometryFilterOuterRings); } @Test public void testGeometryFilterInners() { - FilterExpression expression = parser.parse("inners:0"); + FilterExpression expression = parser.parse("geometry.inners:0"); assertTrue(expression instanceof GeometryFilterInnerRings); - expression = parser.parse("inners:(1..10)"); + expression = parser.parse("geometry.inners:(1..10)"); assertTrue(expression instanceof GeometryFilterInnerRings); } From a10af090ed64faa2eb8ef1dea3e5349883a81a3c Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Thu, 9 Dec 2021 11:52:55 +0100 Subject: [PATCH 05/29] add "geometry.roundness" filter --- .../ohsome/oshdb/filter/FilterParser.java | 8 ++++- .../oshdb/filter/GeometryFilterRoundness.java | 21 +++++++++++++ .../oshdb/filter/ApplyOSMGeometryTest.java | 30 +++++++++++++++++++ .../heigit/ohsome/oshdb/filter/ParseTest.java | 6 ++++ .../ohsome/oshdb/util/geometry/Geo.java | 28 +++++++++++++++++ 5 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterRoundness.java diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java index e864a3b1d..8b28f2c4f 100644 --- a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java @@ -101,6 +101,8 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) { .toScanner("geometry.vertices"); final Parser outers = Patterns.string("geometry.outers").toScanner("geometry.outers"); final Parser inners = Patterns.string("geometry.inners").toScanner("geometry.inners"); + final Parser roundness = Patterns.string("geometry.roundness") + .toScanner("geometry.roundness"); final Parser changeset = Patterns.string("changeset").toScanner("changeset"); final Parser contributor = Patterns.string("contributor").toScanner("contributor"); @@ -241,13 +243,17 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) { final Parser geometryFilterInners = Parsers.sequence( inners, colon, Parsers.or(positiveIntegerRange, number.map(n -> new ValueRange(n, n))) ).map(GeometryFilterInnerRings::new); + final Parser geometryFilterRoundness = Parsers.sequence( + roundness, colon, floatingRange + ).map(GeometryFilterRoundness::new); final Parser geometryFilter = Parsers.or( geometryFilterArea, geometryFilterLength, geometryFilterPerimeter, geometryFilterVertices, geometryFilterOuters, - geometryFilterInners); + geometryFilterInners, + geometryFilterRoundness); // changeset id filters final Parser changesetIdFilter = Parsers.sequence( diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterRoundness.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterRoundness.java new file mode 100644 index 000000000..d6adb013a --- /dev/null +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterRoundness.java @@ -0,0 +1,21 @@ +package org.heigit.ohsome.oshdb.filter; + +import javax.annotation.Nonnull; +import org.heigit.ohsome.oshdb.util.geometry.Geo; + +/** + * A filter which checks the roundness of polygonal OSM feature geometries. + * + *

Uses the Polsby-Popper test score, see + * wikipedia for details.

+ */ +public class GeometryFilterRoundness extends GeometryFilter { + /** + * Creates a new "roundness" filter object. + * + * @param range the allowed range (inclusive) of values to pass the filter + */ + public GeometryFilterRoundness(@Nonnull ValueRange range) { + super(range, GeometryMetricEvaluator.fromLambda(Geo::compactness, "geometry.roundness")); + } +} diff --git a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java index 3b88eb2f8..2faecdd26 100644 --- a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java +++ b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java @@ -17,6 +17,8 @@ import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.LinearRing; import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.io.ParseException; +import org.locationtech.jts.io.WKTReader; /** * Tests the application of filters to OSM geometries. @@ -322,4 +324,32 @@ public void testGeometryFilterInners() { OSHDBBoundingBox.bboxWgs84Coordinates(1, 1, 2, 2)).getExteriorRing() }))); } + + @Test + public void testGeometryFilterRoundness() throws ParseException { + FilterExpression expression = parser.parse("geometry.roundness:(0.8..)"); + OSMEntity entity = createTestOSMEntityWay(new long[] {}); + assertFalse(expression.applyOSMGeometry(entity, + OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 1, 1)) + )); + var regular32gon = "POLYGON ((1.0000003577924967 0, 0.9807856313208454 0.19509039181797777, " + + "0.9238798630684528 0.382683569286347, 0.8314699097961358 0.5555704317984601, " + + "0.7071070341840506 0.7071070341840459, 0.5555704317984657 0.831469909796132, " + + "0.38268356928635316 0.9238798630684503, 0.19509039181798432 0.9807856313208441, " + + "2.4808391268535802e-15 1.0000003577924967, -0.19509039181797946 0.9807856313208451, " + + "-0.3826835692863486 0.9238798630684522, -0.5555704317984616 0.8314699097961348, " + + "-0.7071070341840471 0.7071070341840494, -0.831469909796133 0.5555704317984642, " + + "-0.9238798630684509 0.3826835692863516, -0.9807856313208444 0.19509039181798263, " + + "-1.0000003577924967 7.657140137520206e-16, -0.9807856313208447 -0.19509039181798113, " + + "-0.9238798630684515 -0.38268356928635017, -0.8314699097961339 -0.555570431798463, " + + "-0.7071070341840482 -0.7071070341840483, -0.5555704317984628 -0.831469909796134, " + + "-0.38268356928635044 -0.9238798630684514, -0.1950903918179816 -0.9807856313208446, " + + "6.123236186583946e-17 -1.0000003577924967, 0.19509039181798174 -0.9807856313208446, " + + "0.38268356928635056 -0.9238798630684514, 0.5555704317984631 -0.8314699097961338, " + + "0.7071070341840483 -0.7071070341840482, 0.8314699097961338 -0.555570431798463, " + + "0.9238798630684514 -0.3826835692863505, 0.9807856313208446 -0.19509039181798166, " + + "1.0000003577924967 0))"; + var reader = new WKTReader(); + assertTrue(expression.applyOSMGeometry(entity, reader.read(regular32gon))); + } } diff --git a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ParseTest.java b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ParseTest.java index 3c2fae0a6..70b28eb2b 100644 --- a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ParseTest.java +++ b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ParseTest.java @@ -357,6 +357,12 @@ public void testGeometryFilterInners() { assertTrue(expression instanceof GeometryFilterInnerRings); } + @Test + public void testGeometryFilterRoundness() { + FilterExpression expression = parser.parse("geometry.roundness:(0.8..)"); + assertTrue(expression instanceof GeometryFilterRoundness); + } + @Test public void testChangesetIdFilter() { FilterExpression expression = parser.parse("changeset:42"); diff --git a/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java b/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java index afff8e563..1b05c4572 100644 --- a/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java +++ b/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java @@ -295,6 +295,34 @@ private static double ringArea(LinearRing ring) { return area; } + // ====================== + // = shape calculations = + // ====================== + + /** + * Calculates the "Polsby–Popper test" score of a polygonal geometry. + * + *

+ * If the shape is not polygonal, zero is returned. If a shape constitues of multiple parts (a + * MultiPolygon geometry), the result is the weighted sum of the individual parts' scores. + * See wikipedia for info. + *

+ * + * @param geom the geometry for which to calculate the PP score of. + * @return the compaceness measure of the input shape. A score of 1 indicates maximum compactness + * (i.e. a circle) + */ + public static double compactness(Geometry geom) { + if (!(geom instanceof Polygonal)) { + return 0; + } + var boundaryLength = Geo.lengthOf(geom.getBoundary()); + if (boundaryLength == 0) { + return 0; + } + return 4 * Math.PI * Geo.areaOf(geom) / (boundaryLength * boundaryLength); + } + // ===================== // = geometry clipping = // ===================== From 90de004f348c1c3f69536f3bca3d2c1df57e9c92 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Thu, 9 Dec 2021 19:16:33 +0100 Subject: [PATCH 06/29] [WIP] add squareness filter and evaluation method --- .../ohsome/oshdb/filter/FilterParser.java | 8 +- .../filter/GeometryFilterSquareness.java | 18 ++ .../oshdb/filter/ApplyOSMGeometryTest.java | 46 +++-- .../heigit/ohsome/oshdb/filter/ParseTest.java | 6 + .../ohsome/oshdb/util/geometry/Geo.java | 195 +++++++++++++++++- .../ohsome/oshdb/util/geometry/GeoTest.java | 93 +++++++++ 6 files changed, 347 insertions(+), 19 deletions(-) create mode 100644 oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterSquareness.java diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java index 8b28f2c4f..7f6667541 100644 --- a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java @@ -103,6 +103,8 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) { final Parser inners = Patterns.string("geometry.inners").toScanner("geometry.inners"); final Parser roundness = Patterns.string("geometry.roundness") .toScanner("geometry.roundness"); + final Parser squareness = Patterns.string("geometry.squareness") + .toScanner("geometry.squareness"); final Parser changeset = Patterns.string("changeset").toScanner("changeset"); final Parser contributor = Patterns.string("contributor").toScanner("contributor"); @@ -246,6 +248,9 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) { final Parser geometryFilterRoundness = Parsers.sequence( roundness, colon, floatingRange ).map(GeometryFilterRoundness::new); + final Parser geometryFilterSquareness = Parsers.sequence( + squareness, colon, floatingRange + ).map(GeometryFilterSquareness::new); final Parser geometryFilter = Parsers.or( geometryFilterArea, geometryFilterLength, @@ -253,7 +258,8 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) { geometryFilterVertices, geometryFilterOuters, geometryFilterInners, - geometryFilterRoundness); + geometryFilterRoundness, + geometryFilterSquareness); // changeset id filters final Parser changesetIdFilter = Parsers.sequence( diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterSquareness.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterSquareness.java new file mode 100644 index 000000000..d484f73d4 --- /dev/null +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterSquareness.java @@ -0,0 +1,18 @@ +package org.heigit.ohsome.oshdb.filter; + +import javax.annotation.Nonnull; +import org.heigit.ohsome.oshdb.util.geometry.Geo; + +/** + * A filter which checks the perimeter of polygonal OSM feature geometries. + */ +public class GeometryFilterSquareness extends GeometryFilter { + /** + * Creates a new perimeter filter object. + * + * @param range the allowed range (inclusive) of values to pass the filter + */ + public GeometryFilterSquareness(@Nonnull ValueRange range) { + super(range, GeometryMetricEvaluator.fromLambda(Geo::rectilinearity, "squareness")); + } +} diff --git a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java index 2faecdd26..0dcd0e955 100644 --- a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java +++ b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java @@ -325,6 +325,24 @@ public void testGeometryFilterInners() { }))); } + private final String regular32gon = "POLYGON ((1.0000003577924967 0, 0.9807856313208454 0.19509039181797777, " + + "0.9238798630684528 0.382683569286347, 0.8314699097961358 0.5555704317984601, " + + "0.7071070341840506 0.7071070341840459, 0.5555704317984657 0.831469909796132, " + + "0.38268356928635316 0.9238798630684503, 0.19509039181798432 0.9807856313208441, " + + "2.4808391268535802e-15 1.0000003577924967, -0.19509039181797946 0.9807856313208451, " + + "-0.3826835692863486 0.9238798630684522, -0.5555704317984616 0.8314699097961348, " + + "-0.7071070341840471 0.7071070341840494, -0.831469909796133 0.5555704317984642, " + + "-0.9238798630684509 0.3826835692863516, -0.9807856313208444 0.19509039181798263, " + + "-1.0000003577924967 7.657140137520206e-16, -0.9807856313208447 -0.19509039181798113, " + + "-0.9238798630684515 -0.38268356928635017, -0.8314699097961339 -0.555570431798463, " + + "-0.7071070341840482 -0.7071070341840483, -0.5555704317984628 -0.831469909796134, " + + "-0.38268356928635044 -0.9238798630684514, -0.1950903918179816 -0.9807856313208446, " + + "6.123236186583946e-17 -1.0000003577924967, 0.19509039181798174 -0.9807856313208446, " + + "0.38268356928635056 -0.9238798630684514, 0.5555704317984631 -0.8314699097961338, " + + "0.7071070341840483 -0.7071070341840482, 0.8314699097961338 -0.555570431798463, " + + "0.9238798630684514 -0.3826835692863505, 0.9807856313208446 -0.19509039181798166, " + + "1.0000003577924967 0))"; + @Test public void testGeometryFilterRoundness() throws ParseException { FilterExpression expression = parser.parse("geometry.roundness:(0.8..)"); @@ -332,24 +350,18 @@ public void testGeometryFilterRoundness() throws ParseException { assertFalse(expression.applyOSMGeometry(entity, OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 1, 1)) )); - var regular32gon = "POLYGON ((1.0000003577924967 0, 0.9807856313208454 0.19509039181797777, " - + "0.9238798630684528 0.382683569286347, 0.8314699097961358 0.5555704317984601, " - + "0.7071070341840506 0.7071070341840459, 0.5555704317984657 0.831469909796132, " - + "0.38268356928635316 0.9238798630684503, 0.19509039181798432 0.9807856313208441, " - + "2.4808391268535802e-15 1.0000003577924967, -0.19509039181797946 0.9807856313208451, " - + "-0.3826835692863486 0.9238798630684522, -0.5555704317984616 0.8314699097961348, " - + "-0.7071070341840471 0.7071070341840494, -0.831469909796133 0.5555704317984642, " - + "-0.9238798630684509 0.3826835692863516, -0.9807856313208444 0.19509039181798263, " - + "-1.0000003577924967 7.657140137520206e-16, -0.9807856313208447 -0.19509039181798113, " - + "-0.9238798630684515 -0.38268356928635017, -0.8314699097961339 -0.555570431798463, " - + "-0.7071070341840482 -0.7071070341840483, -0.5555704317984628 -0.831469909796134, " - + "-0.38268356928635044 -0.9238798630684514, -0.1950903918179816 -0.9807856313208446, " - + "6.123236186583946e-17 -1.0000003577924967, 0.19509039181798174 -0.9807856313208446, " - + "0.38268356928635056 -0.9238798630684514, 0.5555704317984631 -0.8314699097961338, " - + "0.7071070341840483 -0.7071070341840482, 0.8314699097961338 -0.555570431798463, " - + "0.9238798630684514 -0.3826835692863505, 0.9807856313208446 -0.19509039181798166, " - + "1.0000003577924967 0))"; var reader = new WKTReader(); assertTrue(expression.applyOSMGeometry(entity, reader.read(regular32gon))); } + + @Test + public void testGeometryFilterSqareness() throws ParseException { + FilterExpression expression = parser.parse("geometry.squareness:(0.8..)"); + OSMEntity entity = createTestOSMEntityWay(new long[] {}); + assertTrue(expression.applyOSMGeometry(entity, + OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 1, 1)) + )); + var reader = new WKTReader(); + assertFalse(expression.applyOSMGeometry(entity, reader.read(regular32gon))); + } } diff --git a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ParseTest.java b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ParseTest.java index 70b28eb2b..0ea98e884 100644 --- a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ParseTest.java +++ b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ParseTest.java @@ -363,6 +363,12 @@ public void testGeometryFilterRoundness() { assertTrue(expression instanceof GeometryFilterRoundness); } + @Test + public void testGeometryFilterSquareness() { + FilterExpression expression = parser.parse("geometry.squareness:(0.8..)"); + assertTrue(expression instanceof GeometryFilterSquareness); + } + @Test public void testChangesetIdFilter() { FilterExpression expression = parser.parse("changeset:42"); diff --git a/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java b/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java index 1b05c4572..28dadbe3c 100644 --- a/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java +++ b/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java @@ -1,5 +1,6 @@ package org.heigit.ohsome.oshdb.util.geometry; +import java.util.ArrayList; import org.heigit.ohsome.oshdb.OSHDBBoundingBox; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; @@ -31,6 +32,38 @@ private Geo() { // = line calculations = // ===================== + /** + * Calculate the approximate distance between two coordinates. + * + *

+ * Uses an equirectangular distance approximation, which works well assuming segments are short. + *

+ * + *

+ * Adjusted to partially account for the spheroidal shape of the earth (WGS84 coordinates). + * See https://gis.stackexchange.com/a/63047/41632 + *

+ * + *

+ * For typical features present in OpenStreetMap data, the relative error introduced by + * this approximation is below 0.1% + *

+ * + * @param c1 one of the two coordinates defining the line segment + * @param c2 the ther coordinate defining the line segment + * @return The approximate geodesic length of the line segment in meters. + */ + public static double distanceBetween(Coordinate c1, Coordinate c2) { + double c1Lon = Math.toRadians(c1.x); + double c1Lat = Math.atan(sphereFact * Math.tan(Math.toRadians(c1.y))); + double c2Lon = Math.toRadians(c2.x); + double c2Lat = Math.atan(sphereFact * Math.tan(Math.toRadians(c2.y))); + double deltaLon = c2Lon - c1Lon; + double deltaLat = c2Lat - c1Lat; + deltaLon *= Math.cos((c2Lat + c1Lat) / 2); + return Math.sqrt(deltaLon * deltaLon + deltaLat * deltaLat) * earthRadiusMean; + } + /** * Calculate the approximate length of a line string. * @@ -52,8 +85,11 @@ private Geo() { * @return The approximate geodesic length of the line string in meters. */ public static double lengthOf(LineString line) { + return lengthOf(line.getCoordinates()); + } + + private static double lengthOf(Coordinate[] coords) { double dist = 0.0; - Coordinate[] coords = line.getCoordinates(); if (coords.length > 1) { double prevLon = Math.toRadians(coords[0].x); @@ -323,6 +359,163 @@ public static double compactness(Geometry geom) { return 4 * Math.PI * Geo.areaOf(geom) / (boundaryLength * boundaryLength); } + // ====================== + // = angle calculations = + // ====================== + + public static double bearing(Coordinate from, Coordinate to) { + return bearingRadians(from, to) * 180 / Math.PI; + } + + public static double bearingRadians(Coordinate from, Coordinate to) { + var y = Math.sin(to.x - from.x) * Math.cos(to.y); + var x = Math.cos(from.y) * Math.sin(to.y) + - Math.sin(from.y) * Math.cos(to.y) * Math.cos(to.x - from.x); + return (Math.atan2(y, x) + 2 * Math.PI) % (2 * Math.PI); + } + + /** + * Returns a measure for the rectilinearity (or squareness) of a geometry. + * + *

Adapted from "A Rectilinearity Measurement for Polygons" by Joviša Žunić and Paul L. Rosin: + * DOI:10.1007/3-540-47967-8_50 + * https://link.springer.com/chapter/10.1007%2F3-540-47967-8_50 + * https://www.researchgate.net/publication/221304067_A_Rectilinearity_Measurement_for_Polygons + * + * Žunić J., Rosin P.L. (2002) A Rectilinearity Measurement for Polygons. In: Heyden A., Sparr G., + * Nielsen M., Johansen P. (eds) Computer Vision — ECCV 2002. ECCV 2002. Lecture Notes in Computer + * Science, vol 2351. Springer, Berlin, Heidelberg. https://doi.org/10.1007/3-540-47967-8_50 + *

+ * + *

Adjusted to work directly on geographic coordinates. Implementation works on a few + * assumptions: input geometries are "small"; spherical globe approximation.

+ * + * @param geom a Polygon, MultiPolygon and LineString for which the squareness is to be evaluated. + * @return returns the rectilinearity value of the input geometry, or zero if the geometry type + * isn't supported + */ + public static double rectilinearity(Geometry geom) { + LineString[] lines; + if (geom instanceof Polygon) { + var poly = (Polygon) geom; + var rings = new ArrayList(poly.getNumInteriorRing() + 1); + rings.add(poly.getExteriorRing()); + for (var i = 0; i < poly.getNumInteriorRing(); i++) { + rings.add(poly.getInteriorRingN(i)); + } + lines = rings.toArray(new LineString[] {}); + } else if (geom instanceof MultiPolygon) { + var multiPoly = (MultiPolygon) geom; + var rings = new ArrayList(); + for (var i = 0; i < multiPoly.getNumGeometries(); i++) { + var poly = (Polygon) geom.getGeometryN(i); + rings.add(poly.getExteriorRing()); + for (var j = 0; j < poly.getNumInteriorRing(); j++) { + rings.add(poly.getInteriorRingN(j)); + } + } + lines = rings.toArray(new LineString[] {}); + } else if (geom instanceof LineString) { + lines = new LineString[] { (LineString) geom }; + } else { + // other geometry types: return 0 + return 0; + } + return rectilinearity(lines); + } + + private static double rectilinearity(LineString[] lines) { + var minLengthL1 = Double.MAX_VALUE; + for (LineString line : lines) { + var coords = line.getCoordinates(); + for (var j = 1; j < coords.length; j++) { + var angle = bearingRadians(coords[j - 1], coords[j]); + var lengthL1 = 0.0; + for (LineString lineAgain : lines) { + lengthL1 += gridAlignedLengthL1(lineAgain, angle); + } + if (lengthL1 < minLengthL1) { + minLengthL1 = lengthL1; + } + } + } + var lengthL2 = 0.0; + for (LineString line : lines) { + lengthL2 += lengthOfL2(line.getCoordinates()); + } + if (minLengthL1 == 0) { + return 0; + } + return 4 / (4 - Math.PI) * (lengthL2 / minLengthL1 - Math.PI / 4); + } + + private static double gridAlignedLengthL1(LineString line, double angle) { + var cosA = Math.cos(angle); + var sinA = Math.sin(angle); + var centroid = line.getCentroid().getCoordinate(); + var cosCentroidY = Math.cos(centroid.y); + var inverseCosCentroidY = 1 / cosCentroidY; + var coords = line.getCoordinates(); + var modifiedCoords = new Coordinate[coords.length]; + // shift to origin + for (var i = 0; i < coords.length; i++) { + modifiedCoords[i] = new Coordinate( + (coords[i].x - centroid.x) * cosCentroidY, + coords[i].y - centroid.y + ); + } + // rotate + for (Coordinate modifiedCoord : modifiedCoords) { + var newX = modifiedCoord.x * cosA - modifiedCoord.y * sinA; + modifiedCoord.y = modifiedCoord.x * sinA + modifiedCoord.y * cosA; + modifiedCoord.x = newX; + } + // shift back to original location + for (Coordinate modifiedCoord : modifiedCoords) { + modifiedCoord.x = modifiedCoord.x * inverseCosCentroidY + centroid.x; + modifiedCoord.y += centroid.y; + } + return lengthOfL1(modifiedCoords); + } + + private static double lengthOfL1(Coordinate[] coords) { + var dist = 0.0; + if (coords.length > 1) { + double prevLon = Math.toRadians(coords[0].x); + double prevLat = Math.toRadians(coords[0].y); + for (var i = 1; i < coords.length; i++) { + double thisLon = Math.toRadians(coords[i].x); + double thisLat = Math.toRadians(coords[i].y); + double deltaLon = thisLon - prevLon; + double deltaLat = thisLat - prevLat; + deltaLon *= Math.cos((thisLat + prevLat) / 2); + dist += Math.abs(deltaLon) + Math.abs(deltaLat); + prevLon = thisLon; + prevLat = thisLat; + } + } + return dist; + } + + private static double lengthOfL2(Coordinate[] coords) { + var dist = 0.0; + if (coords.length > 1) { + double prevLon = Math.toRadians(coords[0].x); + double prevLat = Math.toRadians(coords[0].y); + for (var i = 1; i < coords.length; i++) { + double thisLon = Math.toRadians(coords[i].x); + double thisLat = Math.toRadians(coords[i].y); + double deltaLon = thisLon - prevLon; + double deltaLat = thisLat - prevLat; + deltaLon *= Math.cos((thisLat + prevLat) / 2); + dist += Math.sqrt(deltaLon * deltaLon + deltaLat * deltaLat); + prevLon = thisLon; + prevLat = thisLat; + } + } + return dist; + } + // ===================== // = geometry clipping = // ===================== diff --git a/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java b/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java index 9925ceadb..b8886b731 100644 --- a/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java +++ b/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java @@ -1,6 +1,7 @@ package org.heigit.ohsome.oshdb.util.geometry; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import org.junit.Test; import org.locationtech.jts.geom.Coordinate; @@ -13,6 +14,8 @@ import org.locationtech.jts.geom.MultiPolygon; import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.io.ParseException; +import org.locationtech.jts.io.WKTReader; /** * Tests the {@link Geo} class. @@ -256,6 +259,96 @@ public void testLengthRealFeatures() { assertEquals(1.0, Geo.lengthOf(line) / expectedResult, relativeDelta); } + @Test + public void testRectilinearity() throws ParseException { + final double L = 1E-4; + final double D = 10; + // square + assertEquals(1.0, Geo.rectilinearity(gf.createPolygon(new Coordinate[] { + new Coordinate(0, 0), + new Coordinate(L, 0), + new Coordinate(L, L), + new Coordinate(0, L), + new Coordinate(0, 0) + })), 0.01); + // square tilted + assertEquals(1.0, Geo.rectilinearity(gf.createPolygon(new Coordinate[] { + new Coordinate(L, 0), + new Coordinate(0, L), + new Coordinate(-L, 0), + new Coordinate(0, -L), + new Coordinate(L, 0) + })), 0.01); + // square tilted and shifted + assertEquals(1.0, Geo.rectilinearity(gf.createPolygon(new Coordinate[] { + new Coordinate(L, D), + new Coordinate(0, D + L), + new Coordinate(-L, D), + new Coordinate(0, D - L), + new Coordinate(L, D) + })), 0.01); + // triangle + assertEquals(0.3, Geo.rectilinearity(gf.createPolygon(new Coordinate[] { + new Coordinate(0, 0), + new Coordinate(L, 0), + new Coordinate(L, L), + new Coordinate(0, 0) + })), 0.1); + // circle + var regular32gon = "POLYGON ((1.0000003577924967 0, 0.9807856313208454 0.19509039181797777, " + + "0.9238798630684528 0.382683569286347, 0.8314699097961358 0.5555704317984601, " + + "0.7071070341840506 0.7071070341840459, 0.5555704317984657 0.831469909796132, " + + "0.38268356928635316 0.9238798630684503, 0.19509039181798432 0.9807856313208441, " + + "2.4808391268535802e-15 1.0000003577924967, -0.19509039181797946 0.9807856313208451, " + + "-0.3826835692863486 0.9238798630684522, -0.5555704317984616 0.8314699097961348, " + + "-0.7071070341840471 0.7071070341840494, -0.831469909796133 0.5555704317984642, " + + "-0.9238798630684509 0.3826835692863516, -0.9807856313208444 0.19509039181798263, " + + "-1.0000003577924967 7.657140137520206e-16, -0.9807856313208447 -0.19509039181798113, " + + "-0.9238798630684515 -0.38268356928635017, -0.8314699097961339 -0.555570431798463, " + + "-0.7071070341840482 -0.7071070341840483, -0.5555704317984628 -0.831469909796134, " + + "-0.38268356928635044 -0.9238798630684514, -0.1950903918179816 -0.9807856313208446, " + + "6.123236186583946e-17 -1.0000003577924967, 0.19509039181798174 -0.9807856313208446, " + + "0.38268356928635056 -0.9238798630684514, 0.5555704317984631 -0.8314699097961338, " + + "0.7071070341840483 -0.7071070341840482, 0.8314699097961338 -0.555570431798463, " + + "0.9238798630684514 -0.3826835692863505, 0.9807856313208446 -0.19509039181798166, " + + "1.0000003577924967 0))"; + var reader = new WKTReader(); + assertEquals(0.0, Geo.rectilinearity(gf.createLinearRing( + reader.read(regular32gon).getCoordinates())), 0.1); + + // line + assertEquals(1.0, Geo.rectilinearity(gf.createLineString(new Coordinate[] { + new Coordinate(0, 0), + new Coordinate(L, 0) + })), 0.01); + // line, slanted + assertEquals(1.0, Geo.rectilinearity(gf.createLineString(new Coordinate[] { + new Coordinate(0, 0), + new Coordinate(L, L) + })), 0.01); + // line, right angle + assertEquals(1.0, Geo.rectilinearity(gf.createLineString(new Coordinate[] { + new Coordinate(0, 0), + new Coordinate(L, 0), + new Coordinate(L, L) + })), 0.01); + // line, not right angle + assertNotEquals(1.0, Geo.rectilinearity(gf.createLineString(new Coordinate[] { + new Coordinate(0, 0), + new Coordinate(L, 0), + new Coordinate(0, L) + })), 0.1); + + // multipolygon: squares aligned + assertEquals(1.0, Geo.rectilinearity(gf.createPolygon(new Coordinate[] { + new Coordinate(L, D), + new Coordinate(0, D + L), + new Coordinate(-L, D), + new Coordinate(0, D - L), + new Coordinate(L, D) + })), 0.01); + } + // real world test geometries private static final Coordinate[] featureSmall = { From 54c1e62e036ac62747999b5126f2db8396fb0960 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Thu, 20 Oct 2022 17:06:31 +0200 Subject: [PATCH 07/29] fix javadocs --- .../ohsome/oshdb/filter/GeometryFilterSquareness.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterSquareness.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterSquareness.java index d484f73d4..5cb5a36e8 100644 --- a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterSquareness.java +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterSquareness.java @@ -4,11 +4,16 @@ import org.heigit.ohsome.oshdb.util.geometry.Geo; /** - * A filter which checks the perimeter of polygonal OSM feature geometries. + * A filter which checks the squareness of OSM feature geometries. + * + *

For the measure for the rectilinearity (or squareness) of a geometry, a methods adapted from the + * paper "A Rectilinearity Measurement for Polygons" by Joviša Žunić and Paul L. Rosin + * (DOI:10.1007/3-540-47967-8_50, https://link.springer.com/chapter/10.1007%2F3-540-47967-8_50, + * https://www.researchgate.net/publication/221304067_A_Rectilinearity_Measurement_for_Polygons) is used.

*/ public class GeometryFilterSquareness extends GeometryFilter { /** - * Creates a new perimeter filter object. + * Creates a new squareness filter object. * * @param range the allowed range (inclusive) of values to pass the filter */ From 5cbe5aaf8e8c48b732ba1b28e6774f102460062f Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Thu, 20 Oct 2022 17:40:05 +0200 Subject: [PATCH 08/29] add new geometry filters to changelog --- oshdb-filter/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/oshdb-filter/README.md b/oshdb-filter/README.md index f68c5ce3c..31d414b8e 100644 --- a/oshdb-filter/README.md +++ b/oshdb-filter/README.md @@ -73,6 +73,12 @@ Filters are defined in textual form. A filter expression can be composed out of | `geometry:geom-type` | matches anything which has a geometry of the given type (_point_, _line_, _polygon_, or _other_) | `geometry:polygon` | | `area:(from..to-range)` | matches all features with an area that falls into the given range/interval given as two numbers in decimal or scientific notation separated by `..`. The values are interpreted as square meters (`m²`). The lower or upper limit of the interval may be omitted to select features having an area up to or starting from the given value, respectively. | `area:(123.4..1E6)` | | `length:(from..to-range)` | matches all features with a length that falls into the given range/interval given as two numbers in decimal or scientific notation separated by `..`. The values are interpreted as meters (`m`). The lower or upper limit of the interval may be omitted to select features having an area up to or starting from the given value, respectively. | `length:(100..)` | +| `perimeter:(from..to-range)` | matches all polygons with a perimeter that falls into the given range/interval given as two numbers in decimal or scientific notation separated by `..`. The values are interpreted as meters (`m`). The lower or upper limit of the interval may be omitted to select features having an area up to or starting from the given value, respectively. | `perimeter:(100..)` | +| `geometry.vertices:(from..to-range)` | matches features by the number of points they consist of (the range is given as two integers separated by `..`). | `geometry.vertices:(1..10)` | +| `geometry.inners:number` or `geometry.inners:(from..to-range)` | matches features by the number of holes (inner rings) they have (the range is given as two integers separated by `..`) | `geometry.inners:0` or `geometry.inners:(1..)` | +| `geometry.outers:number` or `geometry.outers:(from..to-range)` | matches features by the number of outer rings they consist of (the range is given as two integers separated by `..`) | `geometry.outers:1` or `geometry.outers:(2..)` | +| `geometry.roundness:(from..to-range)` | matches polyongs which have a _roundness_ (or _compactness_) in the given range of values (given as two numbers in decimal or scientific notation separated by `..`). This is using the ["Polsby–Popper test" score](https://en.wikipedia.org/wiki/Polsby%E2%80%93Popper_test) where all values fall in the interval 0 to 1 and 1 represents a perfect circle. | `geometry.roundness:(0.8..)` | +| `geometry.squareness:(from..to-range)` | matches polyongs which have a _squareness_ in the given range of values (given as two numbers in decimal or scientific notation separated by `..`). This is using the [rectilinearity measurement by Žunić and Rosin](https://www.researchgate.net/publication/221304067_A_Rectilinearity_Measurement_for_Polygons?enrichId=rgreq-e07162bef84d28f007e71687a72fed8e-XXX&enrichSource=Y292ZXJQYWdlOzIyMTMwNDA2NztBUzoxNTUyMDY2MjA4MTUzNjRAMTQxNDAxNTU1MDc3NA%3D%3D&el=1_x_3&_esc=publicationCoverPdf) where all values fall in the interval 0 to 1 and 1 represents a perfectly rectilinear geometry. | `geometry.squareness:(0.8..)` | | `changeset:id` | matches OSM contributions performed in the given OSM changeset. Can only be used in queries using the `OSMContributionView`. | `changeset:42` | | `changeset:(list,of,ids)` | matches OSM contributions performed in one of the given OSM changeset ids. Can only be used in queries using the `OSMContributionView`. | `changeset:(1,42,100)` | | `changeset:(from..to-range)` | matches OSM contributions performed in an OSM changeset falling into the given range of changeset ids. Can only be used in queries using the `OSMContributionView`. | `changeset:(100..200)` | From 390a182b633c419bd0d951ac6934539bda7a1f7b Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Thu, 20 Oct 2022 17:43:39 +0200 Subject: [PATCH 09/29] add to changelog --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 430176bd0..9e7d8f7e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ Changelog * remove class `oshdb-util:util.time.TimestampFormatter` ([#419]) +### new features + +* add new OSHDB filters: `perimeter`, `geometry.vertices`, `geometry.inners`, `geometry.outers`, `geometry.roundness` and `geometry.squareness` ([#436]) + ### bugfixes * change geometry filters to be based on full (unclipped) geometries ([#433]) @@ -27,7 +31,7 @@ Changelog [#426]: https://github.com/GIScience/oshdb/pull/426 [#428]: https://github.com/GIScience/oshdb/pull/428 - +[#436]: https://github.com/GIScience/oshdb/pull/436 ## 0.7.1 From 86c1b5abf8c3de5b2e8c8ca016308714ce067c71 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Thu, 20 Oct 2022 17:55:59 +0200 Subject: [PATCH 10/29] update to junit5 --- .../ohsome/oshdb/filter/ApplyOSMGeometryTest.java | 12 ++++++------ .../heigit/ohsome/oshdb/util/geometry/GeoTest.java | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java index 9a6f2a77b..64d089217 100644 --- a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java +++ b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java @@ -173,7 +173,7 @@ void testGeometryFilterLength() { } @Test - public void testGeometryFilterPerimeter() { + void testGeometryFilterPerimeter() { FilterExpression expression = parser.parse("perimeter:(4..5)"); OSMEntity entity = createTestOSMEntityWay(new long[] {1, 2, 3, 4, 1}); assertFalse(expression.applyOSMGeometry(entity, @@ -196,7 +196,7 @@ public void testGeometryFilterPerimeter() { } @Test - public void testGeometryFilterVertices() { + void testGeometryFilterVertices() { FilterExpression expression = parser.parse("geometry.vertices:(11..13)"); // point assertFalse(expression.applyOSMGeometry( @@ -270,7 +270,7 @@ public void testGeometryFilterVertices() { } @Test - public void testGeometryFilterOuters() { + void testGeometryFilterOuters() { FilterExpression expression = parser.parse("geometry.outers:1"); OSMEntity entity = createTestOSMEntityRelation("type", "multipolygon"); assertFalse(expression.applyOSMGeometry(entity, gf.createMultiPolygon(new Polygon[] { @@ -292,7 +292,7 @@ public void testGeometryFilterOuters() { } @Test - public void testGeometryFilterInners() { + void testGeometryFilterInners() { FilterExpression expression = parser.parse("geometry.inners:0"); OSMEntity entity = createTestOSMEntityRelation("type", "multipolygon"); assertTrue(expression.applyOSMGeometry(entity, @@ -344,7 +344,7 @@ public void testGeometryFilterInners() { + "1.0000003577924967 0))"; @Test - public void testGeometryFilterRoundness() throws ParseException { + void testGeometryFilterRoundness() throws ParseException { FilterExpression expression = parser.parse("geometry.roundness:(0.8..)"); OSMEntity entity = createTestOSMEntityWay(new long[] {}); assertFalse(expression.applyOSMGeometry(entity, @@ -355,7 +355,7 @@ public void testGeometryFilterRoundness() throws ParseException { } @Test - public void testGeometryFilterSqareness() throws ParseException { + void testGeometryFilterSqareness() throws ParseException { FilterExpression expression = parser.parse("geometry.squareness:(0.8..)"); OSMEntity entity = createTestOSMEntityWay(new long[] {}); assertTrue(expression.applyOSMGeometry(entity, diff --git a/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java b/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java index 643e1b340..7435ed706 100644 --- a/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java +++ b/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java @@ -1,6 +1,7 @@ package org.heigit.ohsome.oshdb.util.geometry; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import org.junit.jupiter.api.Test; @@ -279,7 +280,7 @@ void testLengthRealFeatures() { } @Test - public void testRectilinearity() throws ParseException { + void testRectilinearity() throws ParseException { final double L = 1E-4; final double D = 10; // square From 46b5b0a2a6e5727d0c9f1b0ffb47bd301adcbd23 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Thu, 20 Oct 2022 18:13:13 +0200 Subject: [PATCH 11/29] add test for Geo.compactness method --- .../ohsome/oshdb/util/geometry/GeoTest.java | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java b/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java index 7435ed706..e38e2aefc 100644 --- a/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java +++ b/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java @@ -315,23 +315,6 @@ void testRectilinearity() throws ParseException { new Coordinate(0, 0) })), 0.1); // circle - var regular32gon = "POLYGON ((1.0000003577924967 0, 0.9807856313208454 0.19509039181797777, " - + "0.9238798630684528 0.382683569286347, 0.8314699097961358 0.5555704317984601, " - + "0.7071070341840506 0.7071070341840459, 0.5555704317984657 0.831469909796132, " - + "0.38268356928635316 0.9238798630684503, 0.19509039181798432 0.9807856313208441, " - + "2.4808391268535802e-15 1.0000003577924967, -0.19509039181797946 0.9807856313208451, " - + "-0.3826835692863486 0.9238798630684522, -0.5555704317984616 0.8314699097961348, " - + "-0.7071070341840471 0.7071070341840494, -0.831469909796133 0.5555704317984642, " - + "-0.9238798630684509 0.3826835692863516, -0.9807856313208444 0.19509039181798263, " - + "-1.0000003577924967 7.657140137520206e-16, -0.9807856313208447 -0.19509039181798113, " - + "-0.9238798630684515 -0.38268356928635017, -0.8314699097961339 -0.555570431798463, " - + "-0.7071070341840482 -0.7071070341840483, -0.5555704317984628 -0.831469909796134, " - + "-0.38268356928635044 -0.9238798630684514, -0.1950903918179816 -0.9807856313208446, " - + "6.123236186583946e-17 -1.0000003577924967, 0.19509039181798174 -0.9807856313208446, " - + "0.38268356928635056 -0.9238798630684514, 0.5555704317984631 -0.8314699097961338, " - + "0.7071070341840483 -0.7071070341840482, 0.8314699097961338 -0.555570431798463, " - + "0.9238798630684514 -0.3826835692863505, 0.9807856313208446 -0.19509039181798166, " - + "1.0000003577924967 0))"; var reader = new WKTReader(); assertEquals(0.0, Geo.rectilinearity(gf.createLinearRing( reader.read(regular32gon).getCoordinates())), 0.1); @@ -369,6 +352,20 @@ void testRectilinearity() throws ParseException { })), 0.01); } + @Test + void testCompactness() throws ParseException { + // circle + var reader = new WKTReader(); + assertEquals(1.0, Geo.compactness(reader.read(regular32gon)), 0.1); + // triangle + assertEquals(0.5, Geo.compactness(gf.createPolygon(new Coordinate[] { + new Coordinate(0, 0), + new Coordinate(1E-4, 0), + new Coordinate(1E-4, 1E-4), + new Coordinate(0, 0) + })), 0.2); + } + // real world test geometries private static final Coordinate[] featureSmall = { @@ -911,4 +908,22 @@ void testRectilinearity() throws ParseException { new Coordinate(-56.1608058, -84.1572730), new Coordinate(-56.1617903, -84.1590114), new Coordinate(-56.1637542, -84.1609504)}; + private static final String regular32gon = "POLYGON ((" + + "1.0000003577924967 0, 0.9807856313208454 0.19509039181797777, " + + "0.9238798630684528 0.382683569286347, 0.8314699097961358 0.5555704317984601, " + + "0.7071070341840506 0.7071070341840459, 0.5555704317984657 0.831469909796132, " + + "0.38268356928635316 0.9238798630684503, 0.19509039181798432 0.9807856313208441, " + + "2.4808391268535802e-15 1.0000003577924967, -0.19509039181797946 0.9807856313208451, " + + "-0.3826835692863486 0.9238798630684522, -0.5555704317984616 0.8314699097961348, " + + "-0.7071070341840471 0.7071070341840494, -0.831469909796133 0.5555704317984642, " + + "-0.9238798630684509 0.3826835692863516, -0.9807856313208444 0.19509039181798263, " + + "-1.0000003577924967 7.657140137520206e-16, -0.9807856313208447 -0.19509039181798113, " + + "-0.9238798630684515 -0.38268356928635017, -0.8314699097961339 -0.555570431798463, " + + "-0.7071070341840482 -0.7071070341840483, -0.5555704317984628 -0.831469909796134, " + + "-0.38268356928635044 -0.9238798630684514, -0.1950903918179816 -0.9807856313208446, " + + "6.123236186583946e-17 -1.0000003577924967, 0.19509039181798174 -0.9807856313208446, " + + "0.38268356928635056 -0.9238798630684514, 0.5555704317984631 -0.8314699097961338, " + + "0.7071070341840483 -0.7071070341840482, 0.8314699097961338 -0.555570431798463, " + + "0.9238798630684514 -0.3826835692863505, 0.9807856313208446 -0.19509039181798166, " + + "1.0000003577924967 0))"; } From e55aa55c4231bad89174d422210deec4da6dbb7a Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Thu, 20 Oct 2022 18:19:34 +0200 Subject: [PATCH 12/29] minor fix changelog fixes --- CHANGELOG.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8321f09b7..54609fa42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,7 @@ Changelog ### new features -* Allow to flexibly combine (automatic) aggregation methods (like `aggregateByGeometry(…)` or `aggregateByTimestamp()`) with each other and with `filter` or `map`/`flatMap`, regardless of the order of the applied operations ([#451]) - -### new features - +* allow to flexibly combine (automatic) aggregation methods (like `aggregateByGeometry(…)` or `aggregateByTimestamp()`) with each other and with `filter` or `map`/`flatMap`, regardless of the order of the applied operations ([#451]) * add new OSHDB filters: `perimeter`, `geometry.vertices`, `geometry.inners`, `geometry.outers`, `geometry.roundness` and `geometry.squareness` ([#436]) ### bugfixes @@ -36,6 +33,7 @@ Changelog [#419]: https://github.com/GIScience/oshdb/pull/419 [#433]: https://github.com/GIScience/oshdb/issues/433 +[#436]: https://github.com/GIScience/oshdb/pull/436 [#438]: https://github.com/GIScience/oshdb/pull/438 [#441]: https://github.com/GIScience/oshdb/pull/441 [#443]: https://github.com/GIScience/oshdb/pull/443 @@ -45,6 +43,7 @@ Changelog [#453]: https://github.com/GIScience/oshdb/pull/453 [#454]: https://github.com/GIScience/oshdb/pull/454 [#459]: https://github.com/GIScience/oshdb/pull/459 +[#467]: https://github.com/GIScience/oshdb/pull/467 ## 0.7.2 @@ -56,7 +55,6 @@ Changelog [#426]: https://github.com/GIScience/oshdb/pull/426 [#428]: https://github.com/GIScience/oshdb/pull/428 -[#436]: https://github.com/GIScience/oshdb/pull/436 ## 0.7.1 From 78519a15544ba511d827ea83f17b9c35e72160c6 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Thu, 20 Oct 2022 19:30:57 +0200 Subject: [PATCH 13/29] fix error in brearing calculation, add some more test cases --- .../ohsome/oshdb/util/geometry/Geo.java | 12 ++- .../ohsome/oshdb/util/geometry/GeoTest.java | 90 ++++++++++++++++--- 2 files changed, 87 insertions(+), 15 deletions(-) diff --git a/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java b/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java index 4a4417896..253837586 100644 --- a/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java +++ b/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java @@ -373,9 +373,13 @@ public static double bearing(Coordinate from, Coordinate to) { } public static double bearingRadians(Coordinate from, Coordinate to) { - var y = Math.sin(to.x - from.x) * Math.cos(to.y); - var x = Math.cos(from.y) * Math.sin(to.y) - - Math.sin(from.y) * Math.cos(to.y) * Math.cos(to.x - from.x); + var x1 = from.x * Math.PI / 180; + var x2 = to.x * Math.PI / 180; + var y1 = from.y * Math.PI / 180; + var y2 = to.y * Math.PI / 180; + var y = Math.sin(x2 - x1) * Math.cos(y2); + var x = Math.cos(y1) * Math.sin(y2) + - Math.sin(y1) * Math.cos(y2) * Math.cos(x2 - x1); return (Math.atan2(y, x) + 2 * Math.PI) % (2 * Math.PI); } @@ -458,7 +462,7 @@ private static double gridAlignedLengthL1(LineString line, double angle) { var cosA = Math.cos(angle); var sinA = Math.sin(angle); var centroid = line.getCentroid().getCoordinate(); - var cosCentroidY = Math.cos(centroid.y); + var cosCentroidY = Math.cos(centroid.y * Math.PI / 180); var inverseCosCentroidY = 1 / cosCentroidY; var coords = line.getCoordinates(); var modifiedCoords = new Coordinate[coords.length]; diff --git a/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java b/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java index e38e2aefc..5bb124bed 100644 --- a/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java +++ b/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java @@ -291,6 +291,22 @@ void testRectilinearity() throws ParseException { new Coordinate(0, L), new Coordinate(0, 0) })), 0.01); + // square shifted X + assertEquals(1.0, Geo.rectilinearity(gf.createPolygon(new Coordinate[] { + new Coordinate(D, 0), + new Coordinate(D + L, 0), + new Coordinate(D + L, L), + new Coordinate(D, L), + new Coordinate(D, 0) + })), 0.01); + // square shifted Y + assertEquals(1.0, Geo.rectilinearity(gf.createPolygon(new Coordinate[] { + new Coordinate(0, D), + new Coordinate(L, D), + new Coordinate(L, D + L), + new Coordinate(0, D + L), + new Coordinate(0, D) + })), 0.01); // square tilted assertEquals(1.0, Geo.rectilinearity(gf.createPolygon(new Coordinate[] { new Coordinate(L, 0), @@ -299,14 +315,22 @@ void testRectilinearity() throws ParseException { new Coordinate(0, -L), new Coordinate(L, 0) })), 0.01); - // square tilted and shifted + // square tilted and shifted X + assertEquals(1.0, Geo.rectilinearity(gf.createPolygon(new Coordinate[] { + new Coordinate(D + L, 0), + new Coordinate(D, L), + new Coordinate(D - L, 0), + new Coordinate(D, L), + new Coordinate(D + L, 0) + })), 0.01); + // square tilted and shifted Y – note that it is not perfectly rectangular due to the shift assertEquals(1.0, Geo.rectilinearity(gf.createPolygon(new Coordinate[] { new Coordinate(L, D), new Coordinate(0, D + L), new Coordinate(-L, D), new Coordinate(0, D - L), new Coordinate(L, D) - })), 0.01); + })), 0.1); // triangle assertEquals(0.3, Geo.rectilinearity(gf.createPolygon(new Coordinate[] { new Coordinate(0, 0), @@ -316,8 +340,7 @@ void testRectilinearity() throws ParseException { })), 0.1); // circle var reader = new WKTReader(); - assertEquals(0.0, Geo.rectilinearity(gf.createLinearRing( - reader.read(regular32gon).getCoordinates())), 0.1); + assertEquals(0.0, Geo.rectilinearity(reader.read(regular32gon)), 0.1); // line assertEquals(1.0, Geo.rectilinearity(gf.createLineString(new Coordinate[] { @@ -343,13 +366,58 @@ void testRectilinearity() throws ParseException { })), 0.1); // multipolygon: squares aligned - assertEquals(1.0, Geo.rectilinearity(gf.createPolygon(new Coordinate[] { - new Coordinate(L, D), - new Coordinate(0, D + L), - new Coordinate(-L, D), - new Coordinate(0, D - L), - new Coordinate(L, D) - })), 0.01); + assertEquals(1.0, Geo.rectilinearity(gf.createPolygon( + gf.createLinearRing(new Coordinate[] { + new Coordinate(0, 0), + new Coordinate(L, 0), + new Coordinate(L, L), + new Coordinate(0, L), + new Coordinate(0, 0) + }), new LinearRing[] { + gf.createLinearRing(new Coordinate[] { + new Coordinate(L/3, L/3), + new Coordinate(2*L/3, L/3), + new Coordinate(2*L/3, 2*L/3), + new Coordinate(L/3, 2*L/3), + new Coordinate(L/3, L/3) + }) + }) + ), 0.01); + // multipolygon: squares not aligned + assertNotEquals(1.0, Geo.rectilinearity(gf.createPolygon( + gf.createLinearRing(new Coordinate[] { + new Coordinate(0, 0), + new Coordinate(L, 0), + new Coordinate(L, L), + new Coordinate(0, L), + new Coordinate(0, 0) + }), new LinearRing[] { + gf.createLinearRing(new Coordinate[] { + new Coordinate(L/2 + L/4, L/2), + new Coordinate(L/2, L/2 + L/4), + new Coordinate(L/2 - L/4, L/2), + new Coordinate(L/2, L/2 - L/4), + new Coordinate(L/2 + L/4, L/2) + }) + }) + ), 0.01); + + // real world, simple + var example1 = "POLYGON ((38.3460976 49.0294022, 38.3461905 49.0293425, " + + "38.3462795 49.0294021, 38.3461866 49.0294617, 38.3460976 49.0294022))"; + assertEquals(1.0, Geo.rectilinearity(reader.read(example1)), 0.01); + // real world, complex + var example2 = "POLYGON ((38.3437718 49.0276361, 38.3438681 49.0275809, " + + "38.3438179 49.0275432, 38.3440056 49.0274357, 38.3440234 49.0274491, " + + "38.3441311 49.0273874, 38.3439048 49.0272175, 38.343646 49.0273657, " + + "38.3436218 49.0273475, 38.3435477 49.02739, 38.3435676 49.0274049, " + + "38.3435087 49.0274387, 38.3437718 49.0276361))"; + assertEquals(1.0, Geo.rectilinearity(reader.read(example2)), 0.01); + // real world, unsquare + var example3 = "POLYGON ((38.3452974 49.0268873, 38.3453977 49.0268312, " + + "38.3454916 49.026925, 38.3453901 49.0269725, 38.3452974 49.0268873))"; + assertNotEquals(1.0, Geo.rectilinearity(reader.read(example3)), 0.1); + } @Test From c165e370465b657ac6bc1574170477ef9d19a5a2 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Wed, 2 Nov 2022 17:50:21 +0100 Subject: [PATCH 14/29] Apply suggestions from code review Co-authored-by: Johannes Visintini --- CHANGELOG.md | 2 +- oshdb-filter/README.md | 8 ++++---- .../java/org/heigit/ohsome/oshdb/filter/FilterParser.java | 2 ++ .../ohsome/oshdb/filter/GeometryFilterInnerRings.java | 2 +- .../heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java | 8 ++++---- .../java/org/heigit/ohsome/oshdb/util/geometry/Geo.java | 4 ++-- .../org/heigit/ohsome/oshdb/util/geometry/GeoTest.java | 4 ++-- 7 files changed, 16 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54609fa42..0a1ca91aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ Changelog ### new features * allow to flexibly combine (automatic) aggregation methods (like `aggregateByGeometry(…)` or `aggregateByTimestamp()`) with each other and with `filter` or `map`/`flatMap`, regardless of the order of the applied operations ([#451]) -* add new OSHDB filters: `perimeter`, `geometry.vertices`, `geometry.inners`, `geometry.outers`, `geometry.roundness` and `geometry.squareness` ([#436]) +* add new OSHDB filters: `perimeter`, `geometry.vertices`, `geometry.outers`, `geometry.inners`, `geometry.roundness` and `geometry.squareness` ([#436]) ### bugfixes diff --git a/oshdb-filter/README.md b/oshdb-filter/README.md index 1f22de1b4..3306fd860 100644 --- a/oshdb-filter/README.md +++ b/oshdb-filter/README.md @@ -73,12 +73,12 @@ Filters are defined in textual form. A filter expression can be composed out of | `geometry:geom-type` | matches anything which has a geometry of the given type (_point_, _line_, _polygon_, or _other_) | `geometry:polygon` | | `area:(from..to-range)` | matches all features with an area that falls into the given range/interval given as two numbers in decimal or scientific notation separated by `..`. The values are interpreted as square meters (`m²`). The lower or upper limit of the interval may be omitted to select features having an area up to or starting from the given value, respectively. | `area:(123.4..1E6)` | | `length:(from..to-range)` | matches all features with a length that falls into the given range/interval given as two numbers in decimal or scientific notation separated by `..`. The values are interpreted as meters (`m`). The lower or upper limit of the interval may be omitted to select features having an area up to or starting from the given value, respectively. | `length:(100..)` | -| `perimeter:(from..to-range)` | matches all polygons with a perimeter that falls into the given range/interval given as two numbers in decimal or scientific notation separated by `..`. The values are interpreted as meters (`m`). The lower or upper limit of the interval may be omitted to select features having an area up to or starting from the given value, respectively. | `perimeter:(100..)` | +| `perimeter:(from..to-range)` | matches all features with a perimeter that falls into the given range/interval given as two numbers in decimal or scientific notation separated by `..`. The values are interpreted as meters (`m`). The lower or upper limit of the interval may be omitted to select features having an area up to or starting from the given value, respectively. | `perimeter:(100..)` | | `geometry.vertices:(from..to-range)` | matches features by the number of points they consist of (the range is given as two integers separated by `..`). | `geometry.vertices:(1..10)` | -| `geometry.inners:number` or `geometry.inners:(from..to-range)` | matches features by the number of holes (inner rings) they have (the range is given as two integers separated by `..`) | `geometry.inners:0` or `geometry.inners:(1..)` | | `geometry.outers:number` or `geometry.outers:(from..to-range)` | matches features by the number of outer rings they consist of (the range is given as two integers separated by `..`) | `geometry.outers:1` or `geometry.outers:(2..)` | -| `geometry.roundness:(from..to-range)` | matches polyongs which have a _roundness_ (or _compactness_) in the given range of values (given as two numbers in decimal or scientific notation separated by `..`). This is using the ["Polsby–Popper test" score](https://en.wikipedia.org/wiki/Polsby%E2%80%93Popper_test) where all values fall in the interval 0 to 1 and 1 represents a perfect circle. | `geometry.roundness:(0.8..)` | -| `geometry.squareness:(from..to-range)` | matches polyongs which have a _squareness_ in the given range of values (given as two numbers in decimal or scientific notation separated by `..`). This is using the [rectilinearity measurement by Žunić and Rosin](https://www.researchgate.net/publication/221304067_A_Rectilinearity_Measurement_for_Polygons?enrichId=rgreq-e07162bef84d28f007e71687a72fed8e-XXX&enrichSource=Y292ZXJQYWdlOzIyMTMwNDA2NztBUzoxNTUyMDY2MjA4MTUzNjRAMTQxNDAxNTU1MDc3NA%3D%3D&el=1_x_3&_esc=publicationCoverPdf) where all values fall in the interval 0 to 1 and 1 represents a perfectly rectilinear geometry. | `geometry.squareness:(0.8..)` | +| `geometry.inners:number` or `geometry.inners:(from..to-range)` | matches features by the number of holes (inner rings) they have (the range is given as two integers separated by `..`) | `geometry.inners:0` or `geometry.inners:(1..)` | +| `geometry.roundness:(from..to-range)` | matches polygons which have a _roundness_ (or _compactness_) in the given range of values (given as two numbers in decimal or scientific notation separated by `..`). This is using the ["Polsby–Popper test" score](https://en.wikipedia.org/wiki/Polsby%E2%80%93Popper_test) where all values fall in the interval 0 to 1 and 1 represents a perfect circle. | `geometry.roundness:(0.8..)` | +| `geometry.squareness:(from..to-range)` | matches features which have a _squareness_ in the given range of values (given as two numbers in decimal or scientific notation separated by `..`). This is using the [rectilinearity measurement by Žunić and Rosin](https://www.researchgate.net/publication/221304067_A_Rectilinearity_Measurement_for_Polygons?enrichId=rgreq-e07162bef84d28f007e71687a72fed8e-XXX&enrichSource=Y292ZXJQYWdlOzIyMTMwNDA2NztBUzoxNTUyMDY2MjA4MTUzNjRAMTQxNDAxNTU1MDc3NA%3D%3D&el=1_x_3&_esc=publicationCoverPdf) where all values fall in the interval 0 to 1 and 1 represents a perfectly rectilinear geometry. | `geometry.squareness:(0.8..)` | | `changeset:id` | matches OSM contributions performed in the given OSM changeset. Can only be used in queries using the `OSMContributionView`. | `changeset:42` | | `changeset:(list,of,ids)` | matches OSM contributions performed in one of the given OSM changeset ids. Can only be used in queries using the `OSMContributionView`. | `changeset:(1,42,100)` | | `changeset:(from..to-range)` | matches OSM contributions performed in an OSM changeset falling into the given range of changeset ids. Can only be used in queries using the `OSMContributionView`. | `changeset:(100..200)` | diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java index 7f6667541..3a899e6b3 100644 --- a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java @@ -227,6 +227,8 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) { ), Scanners.isChar(')') ); + + // geometry filter final Parser geometryFilterArea = Parsers.sequence( area, colon, floatingRange ).map(GeometryFilterArea::new); diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterInnerRings.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterInnerRings.java index e4cb64c7f..049bcbfd0 100644 --- a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterInnerRings.java +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterInnerRings.java @@ -26,6 +26,6 @@ public GeometryFilterInnerRings(@Nonnull ValueRange range) { } else { return -1; } - }, "geometry.outers")); + }, "geometry.inners")); } } diff --git a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java index 64d089217..0ec60c173 100644 --- a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java +++ b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java @@ -177,20 +177,20 @@ void testGeometryFilterPerimeter() { FilterExpression expression = parser.parse("perimeter:(4..5)"); OSMEntity entity = createTestOSMEntityWay(new long[] {1, 2, 3, 4, 1}); assertFalse(expression.applyOSMGeometry(entity, - // approx 4 x 0.6m + // square with approx 0.6m edge length OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 5E-6, 5E-6)) )); assertTrue(expression.applyOSMGeometry(entity, - // approx 4 x 1.1m + // square with approx 1.1m edge length OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 1E-5, 1E-5)) )); assertFalse(expression.applyOSMGeometry(entity, - // approx 4 x 2.2m + // square with approx 2.2m edge length OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 2E-5, 2E-5)) )); // negated assertTrue(expression.negate().applyOSMGeometry(entity, - // approx 4 x 0.6m + // square with approx 0.6m edge length OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 5E-6, 5E-6)) )); } diff --git a/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java b/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java index 253837586..c4172c87d 100644 --- a/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java +++ b/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java @@ -344,13 +344,13 @@ private static double ringArea(LinearRing ring) { * Calculates the "Polsby–Popper test" score of a polygonal geometry. * *

- * If the shape is not polygonal, zero is returned. If a shape constitues of multiple parts (a + * If the shape is not polygonal, zero is returned. If a shape constitutes of multiple parts (a * MultiPolygon geometry), the result is the weighted sum of the individual parts' scores. * See wikipedia for info. *

* * @param geom the geometry for which to calculate the PP score of. - * @return the compaceness measure of the input shape. A score of 1 indicates maximum compactness + * @return the compactness measure of the input shape. A score of 1 indicates maximum compactness * (i.e. a circle) */ public static double compactness(Geometry geom) { diff --git a/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java b/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java index 5bb124bed..e4151aec6 100644 --- a/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java +++ b/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java @@ -365,7 +365,7 @@ void testRectilinearity() throws ParseException { new Coordinate(0, L) })), 0.1); - // multipolygon: squares aligned + // polygon with holes: squares aligned assertEquals(1.0, Geo.rectilinearity(gf.createPolygon( gf.createLinearRing(new Coordinate[] { new Coordinate(0, 0), @@ -383,7 +383,7 @@ void testRectilinearity() throws ParseException { }) }) ), 0.01); - // multipolygon: squares not aligned + // polygon with holes: squares not aligned assertNotEquals(1.0, Geo.rectilinearity(gf.createPolygon( gf.createLinearRing(new Coordinate[] { new Coordinate(0, 0), From 249e53e1d4c13423ba8053c3ef971ce87baf6602 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Wed, 2 Nov 2022 17:54:11 +0100 Subject: [PATCH 15/29] drop unused/untested methods, rename methods to match filters --- .../oshdb/filter/GeometryFilterRoundness.java | 2 +- .../filter/GeometryFilterSquareness.java | 2 +- .../ohsome/oshdb/util/geometry/Geo.java | 49 +++---------------- .../ohsome/oshdb/util/geometry/GeoTest.java | 38 +++++++------- 4 files changed, 28 insertions(+), 63 deletions(-) diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterRoundness.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterRoundness.java index d6adb013a..a7dd897b7 100644 --- a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterRoundness.java +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterRoundness.java @@ -16,6 +16,6 @@ public class GeometryFilterRoundness extends GeometryFilter { * @param range the allowed range (inclusive) of values to pass the filter */ public GeometryFilterRoundness(@Nonnull ValueRange range) { - super(range, GeometryMetricEvaluator.fromLambda(Geo::compactness, "geometry.roundness")); + super(range, GeometryMetricEvaluator.fromLambda(Geo::roundness, "geometry.roundness")); } } diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterSquareness.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterSquareness.java index 5cb5a36e8..237e61890 100644 --- a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterSquareness.java +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterSquareness.java @@ -18,6 +18,6 @@ public class GeometryFilterSquareness extends GeometryFilter { * @param range the allowed range (inclusive) of values to pass the filter */ public GeometryFilterSquareness(@Nonnull ValueRange range) { - super(range, GeometryMetricEvaluator.fromLambda(Geo::rectilinearity, "squareness")); + super(range, GeometryMetricEvaluator.fromLambda(Geo::squareness, "squareness")); } } diff --git a/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java b/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java index c4172c87d..3e7939c2c 100644 --- a/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java +++ b/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java @@ -32,38 +32,6 @@ private Geo() { // = line calculations = // ===================== - /** - * Calculate the approximate distance between two coordinates. - * - *

- * Uses an equirectangular distance approximation, which works well assuming segments are short. - *

- * - *

- * Adjusted to partially account for the spheroidal shape of the earth (WGS84 coordinates). - * See https://gis.stackexchange.com/a/63047/41632 - *

- * - *

- * For typical features present in OpenStreetMap data, the relative error introduced by - * this approximation is below 0.1% - *

- * - * @param c1 one of the two coordinates defining the line segment - * @param c2 the ther coordinate defining the line segment - * @return The approximate geodesic length of the line segment in meters. - */ - public static double distanceBetween(Coordinate c1, Coordinate c2) { - double c1Lon = Math.toRadians(c1.x); - double c1Lat = Math.atan(sphereFact * Math.tan(Math.toRadians(c1.y))); - double c2Lon = Math.toRadians(c2.x); - double c2Lat = Math.atan(sphereFact * Math.tan(Math.toRadians(c2.y))); - double deltaLon = c2Lon - c1Lon; - double deltaLat = c2Lat - c1Lat; - deltaLon *= Math.cos((c2Lat + c1Lat) / 2); - return Math.sqrt(deltaLon * deltaLon + deltaLat * deltaLat) * earthRadiusMean; - } - /** * Calculate the approximate length of a line string. * @@ -341,7 +309,8 @@ private static double ringArea(LinearRing ring) { // ====================== /** - * Calculates the "Polsby–Popper test" score of a polygonal geometry. + * Calculates the "Polsby–Popper test" score of a polygonal geometry, representing the + * roundness (or compactness) of the feature. * *

* If the shape is not polygonal, zero is returned. If a shape constitutes of multiple parts (a @@ -353,7 +322,7 @@ private static double ringArea(LinearRing ring) { * @return the compactness measure of the input shape. A score of 1 indicates maximum compactness * (i.e. a circle) */ - public static double compactness(Geometry geom) { + public static double roundness(Geometry geom) { if (!(geom instanceof Polygonal)) { return 0; } @@ -368,10 +337,6 @@ public static double compactness(Geometry geom) { // = angle calculations = // ====================== - public static double bearing(Coordinate from, Coordinate to) { - return bearingRadians(from, to) * 180 / Math.PI; - } - public static double bearingRadians(Coordinate from, Coordinate to) { var x1 = from.x * Math.PI / 180; var x2 = to.x * Math.PI / 180; @@ -384,7 +349,7 @@ public static double bearingRadians(Coordinate from, Coordinate to) { } /** - * Returns a measure for the rectilinearity (or squareness) of a geometry. + * Returns a measure for the squareness (or rectilinearity) of a geometry. * *

Adapted from "A Rectilinearity Measurement for Polygons" by Joviša Žunić and Paul L. Rosin: * DOI:10.1007/3-540-47967-8_50 @@ -403,7 +368,7 @@ public static double bearingRadians(Coordinate from, Coordinate to) { * @return returns the rectilinearity value of the input geometry, or zero if the geometry type * isn't supported */ - public static double rectilinearity(Geometry geom) { + public static double squareness(Geometry geom) { LineString[] lines; if (geom instanceof Polygon) { var poly = (Polygon) geom; @@ -430,10 +395,10 @@ public static double rectilinearity(Geometry geom) { // other geometry types: return 0 return 0; } - return rectilinearity(lines); + return squareness(lines); } - private static double rectilinearity(LineString[] lines) { + private static double squareness(LineString[] lines) { var minLengthL1 = Double.MAX_VALUE; for (LineString line : lines) { var coords = line.getCoordinates(); diff --git a/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java b/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java index e4151aec6..9358a6b4a 100644 --- a/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java +++ b/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java @@ -284,7 +284,7 @@ void testRectilinearity() throws ParseException { final double L = 1E-4; final double D = 10; // square - assertEquals(1.0, Geo.rectilinearity(gf.createPolygon(new Coordinate[] { + assertEquals(1.0, Geo.squareness(gf.createPolygon(new Coordinate[] { new Coordinate(0, 0), new Coordinate(L, 0), new Coordinate(L, L), @@ -292,7 +292,7 @@ void testRectilinearity() throws ParseException { new Coordinate(0, 0) })), 0.01); // square shifted X - assertEquals(1.0, Geo.rectilinearity(gf.createPolygon(new Coordinate[] { + assertEquals(1.0, Geo.squareness(gf.createPolygon(new Coordinate[] { new Coordinate(D, 0), new Coordinate(D + L, 0), new Coordinate(D + L, L), @@ -300,7 +300,7 @@ void testRectilinearity() throws ParseException { new Coordinate(D, 0) })), 0.01); // square shifted Y - assertEquals(1.0, Geo.rectilinearity(gf.createPolygon(new Coordinate[] { + assertEquals(1.0, Geo.squareness(gf.createPolygon(new Coordinate[] { new Coordinate(0, D), new Coordinate(L, D), new Coordinate(L, D + L), @@ -308,7 +308,7 @@ void testRectilinearity() throws ParseException { new Coordinate(0, D) })), 0.01); // square tilted - assertEquals(1.0, Geo.rectilinearity(gf.createPolygon(new Coordinate[] { + assertEquals(1.0, Geo.squareness(gf.createPolygon(new Coordinate[] { new Coordinate(L, 0), new Coordinate(0, L), new Coordinate(-L, 0), @@ -316,7 +316,7 @@ void testRectilinearity() throws ParseException { new Coordinate(L, 0) })), 0.01); // square tilted and shifted X - assertEquals(1.0, Geo.rectilinearity(gf.createPolygon(new Coordinate[] { + assertEquals(1.0, Geo.squareness(gf.createPolygon(new Coordinate[] { new Coordinate(D + L, 0), new Coordinate(D, L), new Coordinate(D - L, 0), @@ -324,7 +324,7 @@ void testRectilinearity() throws ParseException { new Coordinate(D + L, 0) })), 0.01); // square tilted and shifted Y – note that it is not perfectly rectangular due to the shift - assertEquals(1.0, Geo.rectilinearity(gf.createPolygon(new Coordinate[] { + assertEquals(1.0, Geo.squareness(gf.createPolygon(new Coordinate[] { new Coordinate(L, D), new Coordinate(0, D + L), new Coordinate(-L, D), @@ -332,7 +332,7 @@ void testRectilinearity() throws ParseException { new Coordinate(L, D) })), 0.1); // triangle - assertEquals(0.3, Geo.rectilinearity(gf.createPolygon(new Coordinate[] { + assertEquals(0.3, Geo.squareness(gf.createPolygon(new Coordinate[] { new Coordinate(0, 0), new Coordinate(L, 0), new Coordinate(L, L), @@ -340,33 +340,33 @@ void testRectilinearity() throws ParseException { })), 0.1); // circle var reader = new WKTReader(); - assertEquals(0.0, Geo.rectilinearity(reader.read(regular32gon)), 0.1); + assertEquals(0.0, Geo.squareness(reader.read(regular32gon)), 0.1); // line - assertEquals(1.0, Geo.rectilinearity(gf.createLineString(new Coordinate[] { + assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[] { new Coordinate(0, 0), new Coordinate(L, 0) })), 0.01); // line, slanted - assertEquals(1.0, Geo.rectilinearity(gf.createLineString(new Coordinate[] { + assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[] { new Coordinate(0, 0), new Coordinate(L, L) })), 0.01); // line, right angle - assertEquals(1.0, Geo.rectilinearity(gf.createLineString(new Coordinate[] { + assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[] { new Coordinate(0, 0), new Coordinate(L, 0), new Coordinate(L, L) })), 0.01); // line, not right angle - assertNotEquals(1.0, Geo.rectilinearity(gf.createLineString(new Coordinate[] { + assertNotEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[] { new Coordinate(0, 0), new Coordinate(L, 0), new Coordinate(0, L) })), 0.1); // polygon with holes: squares aligned - assertEquals(1.0, Geo.rectilinearity(gf.createPolygon( + assertEquals(1.0, Geo.squareness(gf.createPolygon( gf.createLinearRing(new Coordinate[] { new Coordinate(0, 0), new Coordinate(L, 0), @@ -384,7 +384,7 @@ void testRectilinearity() throws ParseException { }) ), 0.01); // polygon with holes: squares not aligned - assertNotEquals(1.0, Geo.rectilinearity(gf.createPolygon( + assertNotEquals(1.0, Geo.squareness(gf.createPolygon( gf.createLinearRing(new Coordinate[] { new Coordinate(0, 0), new Coordinate(L, 0), @@ -405,18 +405,18 @@ void testRectilinearity() throws ParseException { // real world, simple var example1 = "POLYGON ((38.3460976 49.0294022, 38.3461905 49.0293425, " + "38.3462795 49.0294021, 38.3461866 49.0294617, 38.3460976 49.0294022))"; - assertEquals(1.0, Geo.rectilinearity(reader.read(example1)), 0.01); + assertEquals(1.0, Geo.squareness(reader.read(example1)), 0.01); // real world, complex var example2 = "POLYGON ((38.3437718 49.0276361, 38.3438681 49.0275809, " + "38.3438179 49.0275432, 38.3440056 49.0274357, 38.3440234 49.0274491, " + "38.3441311 49.0273874, 38.3439048 49.0272175, 38.343646 49.0273657, " + "38.3436218 49.0273475, 38.3435477 49.02739, 38.3435676 49.0274049, " + "38.3435087 49.0274387, 38.3437718 49.0276361))"; - assertEquals(1.0, Geo.rectilinearity(reader.read(example2)), 0.01); + assertEquals(1.0, Geo.squareness(reader.read(example2)), 0.01); // real world, unsquare var example3 = "POLYGON ((38.3452974 49.0268873, 38.3453977 49.0268312, " + "38.3454916 49.026925, 38.3453901 49.0269725, 38.3452974 49.0268873))"; - assertNotEquals(1.0, Geo.rectilinearity(reader.read(example3)), 0.1); + assertNotEquals(1.0, Geo.squareness(reader.read(example3)), 0.1); } @@ -424,9 +424,9 @@ void testRectilinearity() throws ParseException { void testCompactness() throws ParseException { // circle var reader = new WKTReader(); - assertEquals(1.0, Geo.compactness(reader.read(regular32gon)), 0.1); + assertEquals(1.0, Geo.roundness(reader.read(regular32gon)), 0.1); // triangle - assertEquals(0.5, Geo.compactness(gf.createPolygon(new Coordinate[] { + assertEquals(0.5, Geo.roundness(gf.createPolygon(new Coordinate[] { new Coordinate(0, 0), new Coordinate(1E-4, 0), new Coordinate(1E-4, 1E-4), From 6a1c96bf4c67decc6b352588f57f45c4f43469c4 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Wed, 2 Nov 2022 18:16:30 +0100 Subject: [PATCH 16/29] round input coordinates of test geometry --- .../oshdb/filter/ApplyOSMGeometryTest.java | 26 +++++++----------- .../ohsome/oshdb/util/geometry/GeoTest.java | 27 +++++++------------ 2 files changed, 18 insertions(+), 35 deletions(-) diff --git a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java index 0ec60c173..9bfbe6bf7 100644 --- a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java +++ b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java @@ -325,23 +325,15 @@ void testGeometryFilterInners() { }))); } - private final String regular32gon = "POLYGON ((1.0000003577924967 0, 0.9807856313208454 0.19509039181797777, " - + "0.9238798630684528 0.382683569286347, 0.8314699097961358 0.5555704317984601, " - + "0.7071070341840506 0.7071070341840459, 0.5555704317984657 0.831469909796132, " - + "0.38268356928635316 0.9238798630684503, 0.19509039181798432 0.9807856313208441, " - + "2.4808391268535802e-15 1.0000003577924967, -0.19509039181797946 0.9807856313208451, " - + "-0.3826835692863486 0.9238798630684522, -0.5555704317984616 0.8314699097961348, " - + "-0.7071070341840471 0.7071070341840494, -0.831469909796133 0.5555704317984642, " - + "-0.9238798630684509 0.3826835692863516, -0.9807856313208444 0.19509039181798263, " - + "-1.0000003577924967 7.657140137520206e-16, -0.9807856313208447 -0.19509039181798113, " - + "-0.9238798630684515 -0.38268356928635017, -0.8314699097961339 -0.555570431798463, " - + "-0.7071070341840482 -0.7071070341840483, -0.5555704317984628 -0.831469909796134, " - + "-0.38268356928635044 -0.9238798630684514, -0.1950903918179816 -0.9807856313208446, " - + "6.123236186583946e-17 -1.0000003577924967, 0.19509039181798174 -0.9807856313208446, " - + "0.38268356928635056 -0.9238798630684514, 0.5555704317984631 -0.8314699097961338, " - + "0.7071070341840483 -0.7071070341840482, 0.8314699097961338 -0.555570431798463, " - + "0.9238798630684514 -0.3826835692863505, 0.9807856313208446 -0.19509039181798166, " - + "1.0000003577924967 0))"; + private final String regular32gon = "POLYGON ((1.0000004 0, 0.9807856 0.1950904, " + + "0.9238799 0.3826836, 0.8314699 0.5555704, 0.707107 0.707107, 0.5555704 0.8314699, " + + "0.3826836 0.9238799, 0.1950904 0.9807856, 0 1.0000004, -0.1950904 0.9807856, " + + "-0.3826836 0.9238799, -0.5555704 0.8314699, -0.707107 0.707107, -0.8314699 0.5555704, " + + "-0.9238799 0.3826836, -0.9807856 0.1950904, -1.0000004 0, -0.9807856 -0.1950904, " + + "-0.9238799 -0.3826836, -0.8314699 -0.5555704, -0.707107 -0.707107, -0.5555704 -0.8314699, " + + "-0.3826836 -0.9238799, -0.1950904 -0.9807856, 0 -1.0000004, 0.1950904 -0.9807856, " + + "0.3826836 -0.9238799, 0.5555704 -0.8314699, 0.707107 -0.707107, 0.8314699 -0.5555704, " + + "0.9238799 -0.3826836, 0.9807856 -0.1950904, 1.0000004 0))"; @Test void testGeometryFilterRoundness() throws ParseException { diff --git a/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java b/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java index 9358a6b4a..761f83426 100644 --- a/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java +++ b/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java @@ -976,22 +976,13 @@ void testCompactness() throws ParseException { new Coordinate(-56.1608058, -84.1572730), new Coordinate(-56.1617903, -84.1590114), new Coordinate(-56.1637542, -84.1609504)}; - private static final String regular32gon = "POLYGON ((" - + "1.0000003577924967 0, 0.9807856313208454 0.19509039181797777, " - + "0.9238798630684528 0.382683569286347, 0.8314699097961358 0.5555704317984601, " - + "0.7071070341840506 0.7071070341840459, 0.5555704317984657 0.831469909796132, " - + "0.38268356928635316 0.9238798630684503, 0.19509039181798432 0.9807856313208441, " - + "2.4808391268535802e-15 1.0000003577924967, -0.19509039181797946 0.9807856313208451, " - + "-0.3826835692863486 0.9238798630684522, -0.5555704317984616 0.8314699097961348, " - + "-0.7071070341840471 0.7071070341840494, -0.831469909796133 0.5555704317984642, " - + "-0.9238798630684509 0.3826835692863516, -0.9807856313208444 0.19509039181798263, " - + "-1.0000003577924967 7.657140137520206e-16, -0.9807856313208447 -0.19509039181798113, " - + "-0.9238798630684515 -0.38268356928635017, -0.8314699097961339 -0.555570431798463, " - + "-0.7071070341840482 -0.7071070341840483, -0.5555704317984628 -0.831469909796134, " - + "-0.38268356928635044 -0.9238798630684514, -0.1950903918179816 -0.9807856313208446, " - + "6.123236186583946e-17 -1.0000003577924967, 0.19509039181798174 -0.9807856313208446, " - + "0.38268356928635056 -0.9238798630684514, 0.5555704317984631 -0.8314699097961338, " - + "0.7071070341840483 -0.7071070341840482, 0.8314699097961338 -0.555570431798463, " - + "0.9238798630684514 -0.3826835692863505, 0.9807856313208446 -0.19509039181798166, " - + "1.0000003577924967 0))"; + private static final String regular32gon = "POLYGON ((1.0000004 0, 0.9807856 0.1950904, " + + "0.9238799 0.3826836, 0.8314699 0.5555704, 0.707107 0.707107, 0.5555704 0.8314699, " + + "0.3826836 0.9238799, 0.1950904 0.9807856, 0 1.0000004, -0.1950904 0.9807856, " + + "-0.3826836 0.9238799, -0.5555704 0.8314699, -0.707107 0.707107, -0.8314699 0.5555704, " + + "-0.9238799 0.3826836, -0.9807856 0.1950904, -1.0000004 0, -0.9807856 -0.1950904, " + + "-0.9238799 -0.3826836, -0.8314699 -0.5555704, -0.707107 -0.707107, -0.5555704 -0.8314699, " + + "-0.3826836 -0.9238799, -0.1950904 -0.9807856, 0 -1.0000004, 0.1950904 -0.9807856, " + + "0.3826836 -0.9238799, 0.5555704 -0.8314699, 0.707107 -0.707107, 0.8314699 -0.5555704, " + + "0.9238799 -0.3826836, 0.9807856 -0.1950904, 1.0000004 0))"; } From 9e738c253086d2d56638c5b27f245e1b69bf9a87 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Wed, 2 Nov 2022 18:23:06 +0100 Subject: [PATCH 17/29] rename variable to match "positiveIntegerRange" --- .../org/heigit/ohsome/oshdb/filter/FilterParser.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java index 3a899e6b3..07e48d8d7 100644 --- a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java @@ -203,7 +203,7 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) { Parsers.or(point, line, polygon, other)) .map(geometryType -> new GeometryTypeFilter(geometryType, tt)); - final Parser floatingRange = Parsers.between( + final Parser positiveFloatingRange = Parsers.between( Scanners.isChar('('), Parsers.or( Parsers.sequence(floatingNumber, dotdot, floatingNumber, @@ -230,13 +230,13 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) { // geometry filter final Parser geometryFilterArea = Parsers.sequence( - area, colon, floatingRange + area, colon, positiveFloatingRange ).map(GeometryFilterArea::new); final Parser geometryFilterLength = Parsers.sequence( - length, colon, floatingRange + length, colon, positiveFloatingRange ).map(GeometryFilterLength::new); final Parser geometryFilterPerimeter = Parsers.sequence( - perimeter, colon, floatingRange + perimeter, colon, positiveFloatingRange ).map(GeometryFilterPerimeter::new); final Parser geometryFilterVertices = Parsers.sequence( vertices, colon, positiveIntegerRange @@ -248,10 +248,10 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) { inners, colon, Parsers.or(positiveIntegerRange, number.map(n -> new ValueRange(n, n))) ).map(GeometryFilterInnerRings::new); final Parser geometryFilterRoundness = Parsers.sequence( - roundness, colon, floatingRange + roundness, colon, positiveFloatingRange ).map(GeometryFilterRoundness::new); final Parser geometryFilterSquareness = Parsers.sequence( - squareness, colon, floatingRange + squareness, colon, positiveFloatingRange ).map(GeometryFilterSquareness::new); final Parser geometryFilter = Parsers.or( geometryFilterArea, From b2d99f3d6449dca54358e89f8825b2c133e59ef0 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Wed, 2 Nov 2022 18:25:44 +0100 Subject: [PATCH 18/29] add todo comment --- .../main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java | 1 + 1 file changed, 1 insertion(+) diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java index 07e48d8d7..3456e5e9c 100644 --- a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java @@ -49,6 +49,7 @@ public FilterParser(TagTranslator tt) { * @param allowContributorFilters if true enables filtering by contributor/user id. */ public FilterParser(TagTranslator tt, boolean allowContributorFilters) { + // todo: refactor this method into smaller chunks to make it more easily testable final Parser whitespace = Scanners.WHITESPACES.skipMany(); final Parser keystr = Patterns.regex("[a-zA-Z_0-9:-]+") From b8d109700ffc713c72cb00ebf9335b702019c5f3 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Wed, 2 Nov 2022 18:30:22 +0100 Subject: [PATCH 19/29] extract method for clarity --- .../filter/GeometryFilterInnerRings.java | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterInnerRings.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterInnerRings.java index 049bcbfd0..41ea4a54a 100644 --- a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterInnerRings.java +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/GeometryFilterInnerRings.java @@ -1,6 +1,7 @@ package org.heigit.ohsome.oshdb.filter; import javax.annotation.Nonnull; +import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.MultiPolygon; import org.locationtech.jts.geom.Polygon; @@ -14,18 +15,21 @@ public class GeometryFilterInnerRings extends GeometryFilter { * @param range the allowed range (inclusive) of values to pass the filter */ public GeometryFilterInnerRings(@Nonnull ValueRange range) { - super(range, GeometryMetricEvaluator.fromLambda(geometry -> { - if (geometry instanceof Polygon) { - return ((Polygon) geometry).getNumInteriorRing(); - } else if (geometry instanceof MultiPolygon) { - var counter = 0; - for (var i = 0; i < geometry.getNumGeometries(); i++) { - counter += ((Polygon) geometry.getGeometryN(i)).getNumInteriorRing(); - } - return counter; - } else { - return -1; + super(range, GeometryMetricEvaluator.fromLambda(GeometryFilterInnerRings::countInnerRings, + "geometry.inners")); + } + + private static int countInnerRings(Geometry geometry) { + if (geometry instanceof Polygon) { + return ((Polygon) geometry).getNumInteriorRing(); + } else if (geometry instanceof MultiPolygon) { + var counter = 0; + for (var i = 0; i < geometry.getNumGeometries(); i++) { + counter += ((Polygon) geometry.getGeometryN(i)).getNumInteriorRing(); } - }, "geometry.inners")); + return counter; + } else { + return -1; + } } } From d168a20ffb494b55ec3171d34f91db17e4b2a616 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Wed, 2 Nov 2022 18:34:50 +0100 Subject: [PATCH 20/29] extract often used method combination --- .../oshdb/filter/ApplyOSMGeometryTest.java | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java index 9bfbe6bf7..aaea177c8 100644 --- a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java +++ b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java @@ -26,6 +26,12 @@ class ApplyOSMGeometryTest extends FilterTest { private final GeometryFactory gf = new GeometryFactory(); + private Polygon getBoundingBoxPolygon( + double minLon, double minLat, double maxLon, double maxLat) { + return OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates( + minLon, minLat, maxLon, maxLat)); + } + @Test void testGeometryTypeFilterPoint() { FilterExpression expression = parser.parse("geometry:point"); @@ -115,20 +121,20 @@ void testGeometryFilterArea() { OSMEntity entity = createTestOSMEntityWay(new long[] {1, 2, 3, 4, 1}); assertFalse(expression.applyOSMGeometry(entity, // approx 0.3m² - OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 5E-6, 5E-6)) + getBoundingBoxPolygon(0, 0, 5E-6, 5E-6) )); assertTrue(expression.applyOSMGeometry(entity, // approx 1.2m² - OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 1E-5, 1E-5)) + getBoundingBoxPolygon(0, 0, 1E-5, 1E-5) )); assertFalse(expression.applyOSMGeometry(entity, // approx 4.9m² - OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 2E-5, 2E-5)) + getBoundingBoxPolygon(0, 0, 2E-5, 2E-5) )); // negated assertFalse(expression.negate().applyOSMGeometry(entity, // approx 1.2m² - OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 1E-5, 1E-5)) + getBoundingBoxPolygon(0, 0, 1E-5, 1E-5) )); assertTrue(expression.negate().applyOSMGeometry(entity, // approx 0.3m² @@ -178,20 +184,20 @@ void testGeometryFilterPerimeter() { OSMEntity entity = createTestOSMEntityWay(new long[] {1, 2, 3, 4, 1}); assertFalse(expression.applyOSMGeometry(entity, // square with approx 0.6m edge length - OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 5E-6, 5E-6)) + getBoundingBoxPolygon(0, 0, 5E-6, 5E-6) )); assertTrue(expression.applyOSMGeometry(entity, // square with approx 1.1m edge length - OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 1E-5, 1E-5)) + getBoundingBoxPolygon(0, 0, 1E-5, 1E-5) )); assertFalse(expression.applyOSMGeometry(entity, // square with approx 2.2m edge length - OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 2E-5, 2E-5)) + getBoundingBoxPolygon(0, 0, 2E-5, 2E-5) )); // negated assertTrue(expression.negate().applyOSMGeometry(entity, // square with approx 0.6m edge length - OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 5E-6, 5E-6)) + getBoundingBoxPolygon(0, 0, 5E-6, 5E-6) )); } @@ -258,7 +264,7 @@ void testGeometryFilterVertices() { Stream.of(new Coordinate(1, 1)) ).toArray(Coordinate[]::new); var geometry = gf.createMultiPolygon(new Polygon[] { - OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(-2, -2, -1, -1)), + getBoundingBoxPolygon(-2, -2, -1, -1), gf.createPolygon(coords) }); tester.accept(expression.applyOSMGeometry(entity, geometry)); }; @@ -274,20 +280,20 @@ void testGeometryFilterOuters() { FilterExpression expression = parser.parse("geometry.outers:1"); OSMEntity entity = createTestOSMEntityRelation("type", "multipolygon"); assertFalse(expression.applyOSMGeometry(entity, gf.createMultiPolygon(new Polygon[] { - OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(1, 1, 2, 2)), - OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(3, 3, 4, 4)) + getBoundingBoxPolygon(1, 1, 2, 2), + getBoundingBoxPolygon(3, 3, 4, 4) }))); assertTrue(expression.applyOSMGeometry(entity, gf.createMultiPolygon(new Polygon[] { - OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(1, 1, 2, 2)) + getBoundingBoxPolygon(1, 1, 2, 2) }))); assertTrue(expression.applyOSMGeometry(entity, - OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(1, 1, 2, 2)) + getBoundingBoxPolygon(1, 1, 2, 2) )); // range expression = parser.parse("geometry.outers:(2..)"); assertTrue(expression.applyOSMGeometry(entity, gf.createMultiPolygon(new Polygon[] { - OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(1, 1, 2, 2)), - OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(3, 3, 4, 4)) + getBoundingBoxPolygon(1, 1, 2, 2), + getBoundingBoxPolygon(3, 3, 4, 4) }))); } @@ -296,7 +302,7 @@ void testGeometryFilterInners() { FilterExpression expression = parser.parse("geometry.inners:0"); OSMEntity entity = createTestOSMEntityRelation("type", "multipolygon"); assertTrue(expression.applyOSMGeometry(entity, - OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(1, 1, 2, 2)) + getBoundingBoxPolygon(1, 1, 2, 2) )); assertFalse(expression.applyOSMGeometry(entity, gf.createPolygon( OSHDBGeometryBuilder.getGeometry( @@ -305,7 +311,7 @@ void testGeometryFilterInners() { OSHDBBoundingBox.bboxWgs84Coordinates(1, 1, 2, 2)).getExteriorRing() }))); assertTrue(expression.applyOSMGeometry(entity, gf.createMultiPolygon(new Polygon[] { - OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(1, 1, 2, 2)) + getBoundingBoxPolygon(1, 1, 2, 2) }))); assertFalse(expression.applyOSMGeometry(entity, gf.createMultiPolygon(new Polygon[] { gf.createPolygon( @@ -340,7 +346,7 @@ void testGeometryFilterRoundness() throws ParseException { FilterExpression expression = parser.parse("geometry.roundness:(0.8..)"); OSMEntity entity = createTestOSMEntityWay(new long[] {}); assertFalse(expression.applyOSMGeometry(entity, - OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 1, 1)) + getBoundingBoxPolygon(0, 0, 1, 1) )); var reader = new WKTReader(); assertTrue(expression.applyOSMGeometry(entity, reader.read(regular32gon))); @@ -351,7 +357,7 @@ void testGeometryFilterSqareness() throws ParseException { FilterExpression expression = parser.parse("geometry.squareness:(0.8..)"); OSMEntity entity = createTestOSMEntityWay(new long[] {}); assertTrue(expression.applyOSMGeometry(entity, - OSHDBGeometryBuilder.getGeometry(OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 1, 1)) + getBoundingBoxPolygon(0, 0, 1, 1) )); var reader = new WKTReader(); assertFalse(expression.applyOSMGeometry(entity, reader.read(regular32gon))); From d88e647fc0c4ecf69e1f046ea84ba19448771b0e Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Wed, 2 Nov 2022 18:43:23 +0100 Subject: [PATCH 21/29] add some (internal) comments --- .../org/heigit/ohsome/oshdb/util/geometry/Geo.java | 11 +++++++++++ .../heigit/ohsome/oshdb/util/geometry/GeoTest.java | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java b/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java index 3e7939c2c..3f466d330 100644 --- a/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java +++ b/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java @@ -423,6 +423,9 @@ private static double squareness(LineString[] lines) { return 4 / (4 - Math.PI) * (lengthL2 / minLengthL1 - Math.PI / 4); } + /** + * Intermediate value used in calculation of squareness metric, see paper referenced above. + * */ private static double gridAlignedLengthL1(LineString line, double angle) { var cosA = Math.cos(angle); var sinA = Math.sin(angle); @@ -452,6 +455,10 @@ private static double gridAlignedLengthL1(LineString line, double angle) { return lengthOfL1(modifiedCoords); } + /** + * Intermediate value used in calculation of squareness metric, see paper referenced above + * (this uses the L1 "Manhattan" metric to calculate the length of a given linestring). + * */ private static double lengthOfL1(Coordinate[] coords) { var dist = 0.0; if (coords.length > 1) { @@ -471,6 +478,10 @@ private static double lengthOfL1(Coordinate[] coords) { return dist; } + /** + * Intermediate value used in calculation of squareness metric, see paper referenced above + * (this uses the L2 "euclidean" distance metric to calculate the length of a given linestring). + * */ private static double lengthOfL2(Coordinate[] coords) { var dist = 0.0; if (coords.length > 1) { diff --git a/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java b/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java index 761f83426..651ad8cc4 100644 --- a/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java +++ b/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java @@ -281,8 +281,8 @@ void testLengthRealFeatures() { @Test void testRectilinearity() throws ParseException { - final double L = 1E-4; - final double D = 10; + final double L = 1E-4; // "size" of the test geometries + final double D = 10; // offset used for shifted test geometries // square assertEquals(1.0, Geo.squareness(gf.createPolygon(new Coordinate[] { new Coordinate(0, 0), From d1c1187c1de900cd9261732171ea1d6ba589ed2c Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Wed, 2 Nov 2022 18:46:31 +0100 Subject: [PATCH 22/29] test squareness calculation with LineStrings --- .../ohsome/oshdb/util/geometry/GeoTest.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java b/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java index 651ad8cc4..8783c1b07 100644 --- a/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java +++ b/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java @@ -420,6 +420,40 @@ void testRectilinearity() throws ParseException { } + @Test + void testRectilinearityLineStrings() throws ParseException { + final double L = 1E-4; // "size" of the test geometries + final double D = 10; // offset used for shifted test geometries + // L-shape + assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[]{ + new Coordinate(0, 0), + new Coordinate(L, 0), + new Coordinate(L, L) + })), 0.01); + // S-shape shifted X + assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[]{ + new Coordinate(D, 0), + new Coordinate(D + L, 0), + new Coordinate(D + L, L) + })), 0.01); + // L-shape shifted Y + assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[]{ + new Coordinate(0, D), + new Coordinate(L, D), + new Coordinate(L, D + L) + })), 0.01); + // L-shape tilted + assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[]{ + new Coordinate(L, 0), + new Coordinate(0, L), + new Coordinate(-L, 0) + })), 0.01); + + // circle + var reader = new WKTReader(); + assertEquals(0.0, Geo.squareness(reader.read(regular32gon).getBoundary()), 0.1); + } + @Test void testCompactness() throws ParseException { // circle From 142ad2a161111fe5fe999b6cc142ece84d8eabd4 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Thu, 3 Nov 2022 11:28:56 +0100 Subject: [PATCH 23/29] split and add more tests --- .../oshdb/filter/ApplyOSMGeometryTest.java | 112 +++++++++++++++--- 1 file changed, 97 insertions(+), 15 deletions(-) diff --git a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java index aaea177c8..0df34746c 100644 --- a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java +++ b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java @@ -179,22 +179,36 @@ void testGeometryFilterLength() { } @Test - void testGeometryFilterPerimeter() { + void testGeometryFilterPerimeterTooSmall() { FilterExpression expression = parser.parse("perimeter:(4..5)"); - OSMEntity entity = createTestOSMEntityWay(new long[] {1, 2, 3, 4, 1}); + OSMEntity entity = createTestOSMEntityWay(new long[]{1, 2, 3, 4, 1}); assertFalse(expression.applyOSMGeometry(entity, // square with approx 0.6m edge length getBoundingBoxPolygon(0, 0, 5E-6, 5E-6) )); + } + @Test + void testGeometryFilterPerimeterTooLarge() { + FilterExpression expression = parser.parse("perimeter:(4..5)"); + OSMEntity entity = createTestOSMEntityWay(new long[] {1, 2, 3, 4, 1}); assertTrue(expression.applyOSMGeometry(entity, // square with approx 1.1m edge length getBoundingBoxPolygon(0, 0, 1E-5, 1E-5) )); + } + @Test + void testGeometryFilterPerimeterInRange() { + FilterExpression expression = parser.parse("perimeter:(4..5)"); + OSMEntity entity = createTestOSMEntityWay(new long[] {1, 2, 3, 4, 1}); assertFalse(expression.applyOSMGeometry(entity, // square with approx 2.2m edge length getBoundingBoxPolygon(0, 0, 2E-5, 2E-5) )); - // negated + } + @Test + void testGeometryFilterPerimeterNegated() { + FilterExpression expression = parser.parse("perimeter:(4..5)"); + OSMEntity entity = createTestOSMEntityWay(new long[] {1, 2, 3, 4, 1}); assertTrue(expression.negate().applyOSMGeometry(entity, // square with approx 0.6m edge length getBoundingBoxPolygon(0, 0, 5E-6, 5E-6) @@ -202,13 +216,15 @@ void testGeometryFilterPerimeter() { } @Test - void testGeometryFilterVertices() { + void testGeometryFilterVerticesPoint() { FilterExpression expression = parser.parse("geometry.vertices:(11..13)"); - // point assertFalse(expression.applyOSMGeometry( createTestOSMEntityNode("natural", "tree"), gf.createPoint(new Coordinate(0, 0)))); - // lines + } + @Test + void testGeometryFilterVerticesLine() { + FilterExpression expression = parser.parse("geometry.vertices:(11..13)"); BiConsumer> testLineN = (n, tester) -> { var entity = createTestOSMEntityWay(LongStream.rangeClosed(1, n).toArray()); var coords = LongStream.rangeClosed(1, n) @@ -221,7 +237,10 @@ void testGeometryFilterVertices() { testLineN.accept(12, Assertions::assertTrue); testLineN.accept(13, Assertions::assertTrue); testLineN.accept(14, Assertions::assertFalse); - // polygons + } + @Test + void testGeometryFilterVerticesPolygon() { + FilterExpression expression = parser.parse("geometry.vertices:(11..13)"); BiConsumer> testPolyonN = (n, tester) -> { var entity = createTestOSMEntityWay(LongStream.rangeClosed(1, n).toArray()); var coords = Streams.concat( @@ -236,7 +255,10 @@ void testGeometryFilterVertices() { testPolyonN.accept(12, Assertions::assertTrue); testPolyonN.accept(13, Assertions::assertTrue); testPolyonN.accept(14, Assertions::assertFalse); - // polygon with hole + } + @Test + void testGeometryFilterVerticesPolygonWithHole() { + FilterExpression expression = parser.parse("geometry.vertices:(11..13)"); BiConsumer> testPolyonWithHoleN = (n, tester) -> { var entity = createTestOSMEntityRelation("type", "multipolygon"); n -= 5; // outer shell is a simple bbox with 5 points @@ -255,7 +277,10 @@ void testGeometryFilterVertices() { testPolyonWithHoleN.accept(12, Assertions::assertTrue); testPolyonWithHoleN.accept(13, Assertions::assertTrue); testPolyonWithHoleN.accept(14, Assertions::assertFalse); - // multi polygon + } + @Test + void testGeometryFilterVerticesMultiPolygon() { + FilterExpression expression = parser.parse("geometry.vertices:(11..13)"); BiConsumer> testMultiPolyonN = (n, tester) -> { var entity = createTestOSMEntityRelation("type", "multipolygon"); n -= 5; // outer shell 2 is a simple bbox with 5 points @@ -276,7 +301,37 @@ void testGeometryFilterVertices() { } @Test - void testGeometryFilterOuters() { + void testGeometryFilterOutersPoint() { + FilterExpression expression = parser.parse("geometry.outers:1"); + OSMEntity entity = createTestOSMEntityNode(); + assertFalse(expression.applyOSMGeometry(entity, gf.createPoint(new Coordinate(0, 0)))); + // range + expression = parser.parse("geometry.outers:(2..)"); + assertFalse(expression.applyOSMGeometry(entity, gf.createPoint(new Coordinate(0, 0)))); + } + @Test + void testGeometryFilterOutersLine() { + FilterExpression expression = parser.parse("geometry.outers:1"); + OSMEntity entity = createTestOSMEntityWay(new long[] {1, 2, 3}); + var geom = gf.createLineString(new Coordinate[] { + new Coordinate(0, 0), new Coordinate(1, 0), new Coordinate(1, 1) + }); + assertFalse(expression.applyOSMGeometry(entity, geom)); + // range + expression = parser.parse("geometry.outers:(2..)"); + assertFalse(expression.applyOSMGeometry(entity, geom)); + } + @Test + void testGeometryFilterOutersPolygon() { + FilterExpression expression = parser.parse("geometry.outers:1"); + OSMEntity entity = createTestOSMEntityWay(new long[] {1, 2, 3, 1}); + assertTrue(expression.applyOSMGeometry(entity, getBoundingBoxPolygon(1, 1, 2, 2))); + // range + expression = parser.parse("geometry.outers:(2..)"); + assertFalse(expression.applyOSMGeometry(entity, getBoundingBoxPolygon(1, 1, 2, 2))); + } + @Test + void testGeometryFilterOutersMultiPolygon() { FilterExpression expression = parser.parse("geometry.outers:1"); OSMEntity entity = createTestOSMEntityRelation("type", "multipolygon"); assertFalse(expression.applyOSMGeometry(entity, gf.createMultiPolygon(new Polygon[] { @@ -298,18 +353,45 @@ void testGeometryFilterOuters() { } @Test - void testGeometryFilterInners() { + void testGeometryFilterInnersPoint() { FilterExpression expression = parser.parse("geometry.inners:0"); - OSMEntity entity = createTestOSMEntityRelation("type", "multipolygon"); - assertTrue(expression.applyOSMGeometry(entity, - getBoundingBoxPolygon(1, 1, 2, 2) - )); + OSMEntity entity = createTestOSMEntityNode(); + assertFalse(expression.applyOSMGeometry(entity, gf.createPoint(new Coordinate(0, 0)))); + // range + expression = parser.parse("geometry.inners:(1..)"); + assertFalse(expression.applyOSMGeometry(entity, gf.createPoint(new Coordinate(0, 0)))); + } + @Test + void testGeometryFilterInnersLine() { + FilterExpression expression = parser.parse("geometry.inners:0"); + OSMEntity entity = createTestOSMEntityWay(new long[] {1, 2, 3}); + var geom = gf.createLineString(new Coordinate[] { + new Coordinate(0, 0), new Coordinate(1, 0), new Coordinate(1, 1) + }); + assertFalse(expression.applyOSMGeometry(entity, geom)); + // range + expression = parser.parse("geometry.inners:(1..)"); + assertFalse(expression.applyOSMGeometry(entity, geom)); + } + @Test + void testGeometryFilterInnersPolygon() { + FilterExpression expression = parser.parse("geometry.inners:0"); + OSMEntity entity = createTestOSMEntityWay(new long[] {1, 2, 3, 1}); + assertTrue(expression.applyOSMGeometry(entity, getBoundingBoxPolygon(1, 1, 2, 2))); assertFalse(expression.applyOSMGeometry(entity, gf.createPolygon( OSHDBGeometryBuilder.getGeometry( OSHDBBoundingBox.bboxWgs84Coordinates(0, 0, 10, 10)).getExteriorRing(), new LinearRing[] { OSHDBGeometryBuilder.getGeometry( OSHDBBoundingBox.bboxWgs84Coordinates(1, 1, 2, 2)).getExteriorRing() }))); + // range + expression = parser.parse("geometry.inners:(1..)"); + assertFalse(expression.applyOSMGeometry(entity, getBoundingBoxPolygon(1, 1, 2, 2))); + } + @Test + void testGeometryFilterInnersMultiPolygon() { + FilterExpression expression = parser.parse("geometry.inners:0"); + OSMEntity entity = createTestOSMEntityRelation("type", "multipolygon"); assertTrue(expression.applyOSMGeometry(entity, gf.createMultiPolygon(new Polygon[] { getBoundingBoxPolygon(1, 1, 2, 2) }))); From e9c40761c52e70e13426cd42a2297eee3ac3f1ac Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Thu, 3 Nov 2022 12:07:28 +0100 Subject: [PATCH 24/29] refactor to deduplicate code --- .../ohsome/oshdb/util/geometry/Geo.java | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java b/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java index 3f466d330..e47146c07 100644 --- a/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java +++ b/oshdb-util/src/main/java/org/heigit/ohsome/oshdb/util/geometry/Geo.java @@ -1,6 +1,8 @@ package org.heigit.ohsome.oshdb.util.geometry; import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import org.heigit.ohsome.oshdb.OSHDBBoundingBox; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; @@ -369,36 +371,35 @@ public static double bearingRadians(Coordinate from, Coordinate to) { * isn't supported */ public static double squareness(Geometry geom) { - LineString[] lines; if (geom instanceof Polygon) { - var poly = (Polygon) geom; - var rings = new ArrayList(poly.getNumInteriorRing() + 1); - rings.add(poly.getExteriorRing()); - for (var i = 0; i < poly.getNumInteriorRing(); i++) { - rings.add(poly.getInteriorRingN(i)); - } - lines = rings.toArray(new LineString[] {}); + return squareness(dissolvePolygonToRings((Polygon) geom)); } else if (geom instanceof MultiPolygon) { var multiPoly = (MultiPolygon) geom; var rings = new ArrayList(); for (var i = 0; i < multiPoly.getNumGeometries(); i++) { var poly = (Polygon) geom.getGeometryN(i); - rings.add(poly.getExteriorRing()); - for (var j = 0; j < poly.getNumInteriorRing(); j++) { - rings.add(poly.getInteriorRingN(j)); - } + rings.addAll(dissolvePolygonToRings(poly)); } - lines = rings.toArray(new LineString[] {}); + return squareness(rings); } else if (geom instanceof LineString) { - lines = new LineString[] { (LineString) geom }; + return squareness(Collections.singletonList((LineString) geom)); } else { // other geometry types: return 0 return 0; } - return squareness(lines); } - private static double squareness(LineString[] lines) { + /** Helper method to dissolve a polygon to a collection of rings. */ + private static List dissolvePolygonToRings(Polygon poly) { + var rings = new ArrayList(poly.getNumInteriorRing() + 1); + rings.add(poly.getExteriorRing()); + for (var i = 0; i < poly.getNumInteriorRing(); i++) { + rings.add(poly.getInteriorRingN(i)); + } + return rings; + } + + private static double squareness(List lines) { var minLengthL1 = Double.MAX_VALUE; for (LineString line : lines) { var coords = line.getCoordinates(); From 0b29af61717e6d7e15eb74a1bc28461ecdfe2466 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Thu, 3 Nov 2022 12:25:17 +0100 Subject: [PATCH 25/29] split test cases into nested test class --- .../ohsome/oshdb/util/geometry/GeoTest.java | 393 ++++++++++-------- 1 file changed, 226 insertions(+), 167 deletions(-) diff --git a/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java b/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java index 8783c1b07..9e2756f02 100644 --- a/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java +++ b/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; @@ -279,179 +280,237 @@ void testLengthRealFeatures() { assertEquals(1.0, Geo.lengthOf(line) / expectedResult, relativeDelta); } - @Test - void testRectilinearity() throws ParseException { + @Nested + class RectilinearityTest { final double L = 1E-4; // "size" of the test geometries final double D = 10; // offset used for shifted test geometries - // square - assertEquals(1.0, Geo.squareness(gf.createPolygon(new Coordinate[] { - new Coordinate(0, 0), - new Coordinate(L, 0), - new Coordinate(L, L), - new Coordinate(0, L), - new Coordinate(0, 0) - })), 0.01); - // square shifted X - assertEquals(1.0, Geo.squareness(gf.createPolygon(new Coordinate[] { - new Coordinate(D, 0), - new Coordinate(D + L, 0), - new Coordinate(D + L, L), - new Coordinate(D, L), - new Coordinate(D, 0) - })), 0.01); - // square shifted Y - assertEquals(1.0, Geo.squareness(gf.createPolygon(new Coordinate[] { - new Coordinate(0, D), - new Coordinate(L, D), - new Coordinate(L, D + L), - new Coordinate(0, D + L), - new Coordinate(0, D) - })), 0.01); - // square tilted - assertEquals(1.0, Geo.squareness(gf.createPolygon(new Coordinate[] { - new Coordinate(L, 0), - new Coordinate(0, L), - new Coordinate(-L, 0), - new Coordinate(0, -L), - new Coordinate(L, 0) - })), 0.01); - // square tilted and shifted X - assertEquals(1.0, Geo.squareness(gf.createPolygon(new Coordinate[] { - new Coordinate(D + L, 0), - new Coordinate(D, L), - new Coordinate(D - L, 0), - new Coordinate(D, L), - new Coordinate(D + L, 0) - })), 0.01); - // square tilted and shifted Y – note that it is not perfectly rectangular due to the shift - assertEquals(1.0, Geo.squareness(gf.createPolygon(new Coordinate[] { - new Coordinate(L, D), - new Coordinate(0, D + L), - new Coordinate(-L, D), - new Coordinate(0, D - L), - new Coordinate(L, D) - })), 0.1); - // triangle - assertEquals(0.3, Geo.squareness(gf.createPolygon(new Coordinate[] { - new Coordinate(0, 0), - new Coordinate(L, 0), - new Coordinate(L, L), - new Coordinate(0, 0) - })), 0.1); - // circle - var reader = new WKTReader(); - assertEquals(0.0, Geo.squareness(reader.read(regular32gon)), 0.1); - // line - assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[] { - new Coordinate(0, 0), - new Coordinate(L, 0) - })), 0.01); - // line, slanted - assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[] { - new Coordinate(0, 0), - new Coordinate(L, L) - })), 0.01); - // line, right angle - assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[] { - new Coordinate(0, 0), - new Coordinate(L, 0), - new Coordinate(L, L) - })), 0.01); - // line, not right angle - assertNotEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[] { - new Coordinate(0, 0), - new Coordinate(L, 0), - new Coordinate(0, L) - })), 0.1); - - // polygon with holes: squares aligned - assertEquals(1.0, Geo.squareness(gf.createPolygon( - gf.createLinearRing(new Coordinate[] { - new Coordinate(0, 0), - new Coordinate(L, 0), - new Coordinate(L, L), - new Coordinate(0, L), - new Coordinate(0, 0) - }), new LinearRing[] { - gf.createLinearRing(new Coordinate[] { - new Coordinate(L/3, L/3), - new Coordinate(2*L/3, L/3), - new Coordinate(2*L/3, 2*L/3), - new Coordinate(L/3, 2*L/3), - new Coordinate(L/3, L/3) - }) - }) - ), 0.01); - // polygon with holes: squares not aligned - assertNotEquals(1.0, Geo.squareness(gf.createPolygon( - gf.createLinearRing(new Coordinate[] { - new Coordinate(0, 0), - new Coordinate(L, 0), - new Coordinate(L, L), - new Coordinate(0, L), - new Coordinate(0, 0) - }), new LinearRing[] { - gf.createLinearRing(new Coordinate[] { - new Coordinate(L/2 + L/4, L/2), - new Coordinate(L/2, L/2 + L/4), - new Coordinate(L/2 - L/4, L/2), - new Coordinate(L/2, L/2 - L/4), - new Coordinate(L/2 + L/4, L/2) - }) - }) - ), 0.01); - - // real world, simple - var example1 = "POLYGON ((38.3460976 49.0294022, 38.3461905 49.0293425, " - + "38.3462795 49.0294021, 38.3461866 49.0294617, 38.3460976 49.0294022))"; - assertEquals(1.0, Geo.squareness(reader.read(example1)), 0.01); - // real world, complex - var example2 = "POLYGON ((38.3437718 49.0276361, 38.3438681 49.0275809, " - + "38.3438179 49.0275432, 38.3440056 49.0274357, 38.3440234 49.0274491, " - + "38.3441311 49.0273874, 38.3439048 49.0272175, 38.343646 49.0273657, " - + "38.3436218 49.0273475, 38.3435477 49.02739, 38.3435676 49.0274049, " - + "38.3435087 49.0274387, 38.3437718 49.0276361))"; - assertEquals(1.0, Geo.squareness(reader.read(example2)), 0.01); - // real world, unsquare - var example3 = "POLYGON ((38.3452974 49.0268873, 38.3453977 49.0268312, " - + "38.3454916 49.026925, 38.3453901 49.0269725, 38.3452974 49.0268873))"; - assertNotEquals(1.0, Geo.squareness(reader.read(example3)), 0.1); + @Test + void testSquare() { + assertEquals(1.0, Geo.squareness(gf.createPolygon(new Coordinate[]{ + new Coordinate(0, 0), + new Coordinate(L, 0), + new Coordinate(L, L), + new Coordinate(0, L), + new Coordinate(0, 0) + })), 0.01); + } - } + @Test + void testSquareShiftedX() { + assertEquals(1.0, Geo.squareness(gf.createPolygon(new Coordinate[]{ + new Coordinate(D, 0), + new Coordinate(D + L, 0), + new Coordinate(D + L, L), + new Coordinate(D, L), + new Coordinate(D, 0) + })), 0.01); + } - @Test - void testRectilinearityLineStrings() throws ParseException { - final double L = 1E-4; // "size" of the test geometries - final double D = 10; // offset used for shifted test geometries - // L-shape - assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[]{ - new Coordinate(0, 0), - new Coordinate(L, 0), - new Coordinate(L, L) - })), 0.01); - // S-shape shifted X - assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[]{ - new Coordinate(D, 0), - new Coordinate(D + L, 0), - new Coordinate(D + L, L) - })), 0.01); - // L-shape shifted Y - assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[]{ - new Coordinate(0, D), - new Coordinate(L, D), - new Coordinate(L, D + L) - })), 0.01); - // L-shape tilted - assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[]{ - new Coordinate(L, 0), - new Coordinate(0, L), - new Coordinate(-L, 0) - })), 0.01); + @Test + void testSquareShiftedY() { + assertEquals(1.0, Geo.squareness(gf.createPolygon(new Coordinate[]{ + new Coordinate(0, D), + new Coordinate(L, D), + new Coordinate(L, D + L), + new Coordinate(0, D + L), + new Coordinate(0, D) + })), 0.01); + } - // circle - var reader = new WKTReader(); - assertEquals(0.0, Geo.squareness(reader.read(regular32gon).getBoundary()), 0.1); + @Test + void testSquareTilted() { + assertEquals(1.0, Geo.squareness(gf.createPolygon(new Coordinate[]{ + new Coordinate(L, 0), + new Coordinate(0, L), + new Coordinate(-L, 0), + new Coordinate(0, -L), + new Coordinate(L, 0) + })), 0.01); + } + + @Test + void testSquareTiltedShiftedX() { + assertEquals(1.0, Geo.squareness(gf.createPolygon(new Coordinate[]{ + new Coordinate(D + L, 0), + new Coordinate(D, L), + new Coordinate(D - L, 0), + new Coordinate(D, L), + new Coordinate(D + L, 0) + })), 0.01); + } + + @Test + void testSquareTiltedShiftedY() { + assertEquals(1.0, Geo.squareness(gf.createPolygon(new Coordinate[]{ + new Coordinate(L, D), + new Coordinate(0, D + L), + new Coordinate(-L, D), + new Coordinate(0, D - L), + new Coordinate(L, D) + })), 0.1); + } + + @Test + void testTriangle() { + assertEquals(0.3, Geo.squareness(gf.createPolygon(new Coordinate[]{ + new Coordinate(0, 0), + new Coordinate(L, 0), + new Coordinate(L, L), + new Coordinate(0, 0) + })), 0.1); + } + + @Test + void testCircle() throws ParseException { + var reader = new WKTReader(); + assertEquals(0.0, Geo.squareness(reader.read(regular32gon)), 0.1); + } + + @Test + void testLine() { + assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[]{ + new Coordinate(0, 0), + new Coordinate(L, 0) + })), 0.01); + } + + @Test + void testLineSlanted() { + assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[]{ + new Coordinate(0, 0), + new Coordinate(L, L) + })), 0.01); + } + + @Test + void testLineRightAngle() { + assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[]{ + new Coordinate(0, 0), + new Coordinate(L, 0), + new Coordinate(L, L) + })), 0.01); + } + + @Test + void testLineNotRightAngle() { + assertNotEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[]{ + new Coordinate(0, 0), + new Coordinate(L, 0), + new Coordinate(0, L) + })), 0.1); + } + + @Test + void testPolygonWithAlignedHoles() { + // polygon with holes: squares aligned + assertEquals(1.0, Geo.squareness(gf.createPolygon( + gf.createLinearRing(new Coordinate[]{ + new Coordinate(0, 0), + new Coordinate(L, 0), + new Coordinate(L, L), + new Coordinate(0, L), + new Coordinate(0, 0) + }), new LinearRing[]{ + gf.createLinearRing(new Coordinate[]{ + new Coordinate(L / 3, L / 3), + new Coordinate(2 * L / 3, L / 3), + new Coordinate(2 * L / 3, 2 * L / 3), + new Coordinate(L / 3, 2 * L / 3), + new Coordinate(L / 3, L / 3) + }) + }) + ), 0.01); + } + + @Test + void testPolygonWithUnalignedHoles() { + // polygon with holes: squares not aligned + assertNotEquals(1.0, Geo.squareness(gf.createPolygon( + gf.createLinearRing(new Coordinate[]{ + new Coordinate(0, 0), + new Coordinate(L, 0), + new Coordinate(L, L), + new Coordinate(0, L), + new Coordinate(0, 0) + }), new LinearRing[]{ + gf.createLinearRing(new Coordinate[]{ + new Coordinate(L / 2 + L / 4, L / 2), + new Coordinate(L / 2, L / 2 + L / 4), + new Coordinate(L / 2 - L / 4, L / 2), + new Coordinate(L / 2, L / 2 - L / 4), + new Coordinate(L / 2 + L / 4, L / 2) + }) + }) + ), 0.01); + } + + @Test + void testRectilinearityLineString() { + // L-shape + assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[]{ + new Coordinate(0, 0), + new Coordinate(L, 0), + new Coordinate(L, L) + })), 0.01); + } + + @Test + void testRectilinearityLineStringShiftedX() { + // L-shape shifted X + assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[]{ + new Coordinate(D, 0), + new Coordinate(D + L, 0), + new Coordinate(D + L, L) + })), 0.01); + } + + @Test + void testRectilinearityLineStringShiftedY() { + // L-shape shifted Y + assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[]{ + new Coordinate(0, D), + new Coordinate(L, D), + new Coordinate(L, D + L) + })), 0.01); + } + + @Test + void testRectilinearityLineStringTilted() { + // L-shape tilted + assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[]{ + new Coordinate(L, 0), + new Coordinate(0, L), + new Coordinate(-L, 0) + })), 0.01); + } + + @Test + void testRectilinearityLineStringCircle() throws ParseException { + // circle + var reader = new WKTReader(); + assertEquals(0.0, Geo.squareness(reader.read(regular32gon).getBoundary()), 0.1); + } + + @Test + void testRealWorldObject() throws ParseException { + var reader = new WKTReader(); + // real world, simple + var example1 = "POLYGON ((38.3460976 49.0294022, 38.3461905 49.0293425, " + + "38.3462795 49.0294021, 38.3461866 49.0294617, 38.3460976 49.0294022))"; + assertEquals(1.0, Geo.squareness(reader.read(example1)), 0.01); + // real world, complex + var example2 = "POLYGON ((38.3437718 49.0276361, 38.3438681 49.0275809, " + + "38.3438179 49.0275432, 38.3440056 49.0274357, 38.3440234 49.0274491, " + + "38.3441311 49.0273874, 38.3439048 49.0272175, 38.343646 49.0273657, " + + "38.3436218 49.0273475, 38.3435477 49.02739, 38.3435676 49.0274049, " + + "38.3435087 49.0274387, 38.3437718 49.0276361))"; + assertEquals(1.0, Geo.squareness(reader.read(example2)), 0.01); + // real world, unsquare + var example3 = "POLYGON ((38.3452974 49.0268873, 38.3453977 49.0268312, " + + "38.3454916 49.026925, 38.3453901 49.0269725, 38.3452974 49.0268873))"; + assertNotEquals(1.0, Geo.squareness(reader.read(example3)), 0.1); + } } @Test From b779c7ca25c0c83de01723b94dd12a7b37e74008 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Thu, 3 Nov 2022 13:33:08 +0100 Subject: [PATCH 26/29] (minor) whitespace cleanup --- .../main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java index 3456e5e9c..bc5bd4cdd 100644 --- a/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java +++ b/oshdb-filter/src/main/java/org/heigit/ohsome/oshdb/filter/FilterParser.java @@ -228,7 +228,7 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) { ), Scanners.isChar(')') ); - + // geometry filter final Parser geometryFilterArea = Parsers.sequence( area, colon, positiveFloatingRange From c9eb25dc05618eb8bcff15e90431cf2e8c672437 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Thu, 3 Nov 2022 14:22:57 +0100 Subject: [PATCH 27/29] shorten link --- oshdb-filter/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oshdb-filter/README.md b/oshdb-filter/README.md index 3306fd860..ff6bd2fb2 100644 --- a/oshdb-filter/README.md +++ b/oshdb-filter/README.md @@ -78,7 +78,7 @@ Filters are defined in textual form. A filter expression can be composed out of | `geometry.outers:number` or `geometry.outers:(from..to-range)` | matches features by the number of outer rings they consist of (the range is given as two integers separated by `..`) | `geometry.outers:1` or `geometry.outers:(2..)` | | `geometry.inners:number` or `geometry.inners:(from..to-range)` | matches features by the number of holes (inner rings) they have (the range is given as two integers separated by `..`) | `geometry.inners:0` or `geometry.inners:(1..)` | | `geometry.roundness:(from..to-range)` | matches polygons which have a _roundness_ (or _compactness_) in the given range of values (given as two numbers in decimal or scientific notation separated by `..`). This is using the ["Polsby–Popper test" score](https://en.wikipedia.org/wiki/Polsby%E2%80%93Popper_test) where all values fall in the interval 0 to 1 and 1 represents a perfect circle. | `geometry.roundness:(0.8..)` | -| `geometry.squareness:(from..to-range)` | matches features which have a _squareness_ in the given range of values (given as two numbers in decimal or scientific notation separated by `..`). This is using the [rectilinearity measurement by Žunić and Rosin](https://www.researchgate.net/publication/221304067_A_Rectilinearity_Measurement_for_Polygons?enrichId=rgreq-e07162bef84d28f007e71687a72fed8e-XXX&enrichSource=Y292ZXJQYWdlOzIyMTMwNDA2NztBUzoxNTUyMDY2MjA4MTUzNjRAMTQxNDAxNTU1MDc3NA%3D%3D&el=1_x_3&_esc=publicationCoverPdf) where all values fall in the interval 0 to 1 and 1 represents a perfectly rectilinear geometry. | `geometry.squareness:(0.8..)` | +| `geometry.squareness:(from..to-range)` | matches features which have a _squareness_ in the given range of values (given as two numbers in decimal or scientific notation separated by `..`). This is using the [rectilinearity measurement by Žunić and Rosin](https://www.researchgate.net/publication/221304067_A_Rectilinearity_Measurement_for_Polygons) where all values fall in the interval 0 to 1 and 1 represents a perfectly rectilinear geometry. | `geometry.squareness:(0.8..)` | | `changeset:id` | matches OSM contributions performed in the given OSM changeset. Can only be used in queries using the `OSMContributionView`. | `changeset:42` | | `changeset:(list,of,ids)` | matches OSM contributions performed in one of the given OSM changeset ids. Can only be used in queries using the `OSMContributionView`. | `changeset:(1,42,100)` | | `changeset:(from..to-range)` | matches OSM contributions performed in an OSM changeset falling into the given range of changeset ids. Can only be used in queries using the `OSMContributionView`. | `changeset:(100..200)` | From f8ffd21b6d91f11bc7ab24d5478fd621a3872068 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Thu, 3 Nov 2022 14:47:52 +0100 Subject: [PATCH 28/29] add multipolygon test cases for squareness method --- .../ohsome/oshdb/util/geometry/GeoTest.java | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java b/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java index 9e2756f02..c5aee3b8e 100644 --- a/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java +++ b/oshdb-util/src/test/java/org/heigit/ohsome/oshdb/util/geometry/GeoTest.java @@ -446,7 +446,49 @@ void testPolygonWithUnalignedHoles() { } @Test - void testRectilinearityLineString() { + void testMultiPolygonAligned() { + // multi polygon: two aligned squares + assertEquals(1.0, Geo.squareness(gf.createMultiPolygon(new Polygon[] { + gf.createPolygon(gf.createLinearRing(new Coordinate[]{ + new Coordinate(0, 0), + new Coordinate(L, 0), + new Coordinate(L, L), + new Coordinate(0, L), + new Coordinate(0, 0) + })), + gf.createPolygon(gf.createLinearRing(new Coordinate[]{ + new Coordinate(2 * L, 0), + new Coordinate(3 * L, 0), + new Coordinate(3 * L, L), + new Coordinate(2 * L, L), + new Coordinate(2 * L, 0) + })) + })), 0.01); + } + + @Test + void testMultiPolygonUnaligned() { + // multi polygon: two non-aligned squares + assertNotEquals(1.0, Geo.squareness(gf.createMultiPolygon(new Polygon[] { + gf.createPolygon(gf.createLinearRing(new Coordinate[]{ + new Coordinate(L, 0), + new Coordinate(0, L), + new Coordinate(-L, 0), + new Coordinate(0, -L), + new Coordinate(L, 0) + })), + gf.createPolygon(gf.createLinearRing(new Coordinate[]{ + new Coordinate(2 * L, 0), + new Coordinate(3 * L, 0), + new Coordinate(3 * L, L), + new Coordinate(2 * L, L), + new Coordinate(2 * L, 0) + })) + })), 0.01); + } + + @Test + void testLineString() { // L-shape assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[]{ new Coordinate(0, 0), @@ -456,7 +498,7 @@ void testRectilinearityLineString() { } @Test - void testRectilinearityLineStringShiftedX() { + void testLineStringShiftedX() { // L-shape shifted X assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[]{ new Coordinate(D, 0), @@ -466,7 +508,7 @@ void testRectilinearityLineStringShiftedX() { } @Test - void testRectilinearityLineStringShiftedY() { + void testLineStringShiftedY() { // L-shape shifted Y assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[]{ new Coordinate(0, D), @@ -476,7 +518,7 @@ void testRectilinearityLineStringShiftedY() { } @Test - void testRectilinearityLineStringTilted() { + void testLineStringTilted() { // L-shape tilted assertEquals(1.0, Geo.squareness(gf.createLineString(new Coordinate[]{ new Coordinate(L, 0), @@ -486,7 +528,7 @@ void testRectilinearityLineStringTilted() { } @Test - void testRectilinearityLineStringCircle() throws ParseException { + void testLineStringCircle() throws ParseException { // circle var reader = new WKTReader(); assertEquals(0.0, Geo.squareness(reader.read(regular32gon).getBoundary()), 0.1); From 28cb88bf3d04e7c9b75e5c397486ab191201fa07 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Thu, 3 Nov 2022 16:44:21 +0100 Subject: [PATCH 29/29] Apply suggestions from code review Co-authored-by: Johannes Visintini --- .../org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java index 0df34746c..68a4d2c56 100644 --- a/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java +++ b/oshdb-filter/src/test/java/org/heigit/ohsome/oshdb/filter/ApplyOSMGeometryTest.java @@ -188,7 +188,7 @@ void testGeometryFilterPerimeterTooSmall() { )); } @Test - void testGeometryFilterPerimeterTooLarge() { + void testGeometryFilterPerimeterInRange() { FilterExpression expression = parser.parse("perimeter:(4..5)"); OSMEntity entity = createTestOSMEntityWay(new long[] {1, 2, 3, 4, 1}); assertTrue(expression.applyOSMGeometry(entity, @@ -197,7 +197,7 @@ void testGeometryFilterPerimeterTooLarge() { )); } @Test - void testGeometryFilterPerimeterInRange() { + void testGeometryFilterPerimeterTooLarge() { FilterExpression expression = parser.parse("perimeter:(4..5)"); OSMEntity entity = createTestOSMEntityWay(new long[] {1, 2, 3, 4, 1}); assertFalse(expression.applyOSMGeometry(entity,