From 66ef217647e14ab8f72c8ba6573267997b05d103 Mon Sep 17 00:00:00 2001
From: Martin Raifer <martin.raifer@heigit.org>
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<Void> area = Patterns.string("area").toScanner("area");
     final Parser<Void> length = Patterns.string("length").toScanner("length");
+    final Parser<Void> perimeter = Patterns.string("perimeter").toScanner("perimeter");
     final Parser<Void> changeset = Patterns.string("changeset").toScanner("changeset");
     final Parser<Void> contributor = Patterns.string("contributor").toScanner("contributor");
 
@@ -212,9 +213,13 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) {
     final Parser<GeometryFilter> geometryFilterLength = Parsers.sequence(
         length, colon, floatingRange
     ).map(GeometryFilterLength::new);
+    final Parser<GeometryFilter> geometryFilterPerimeter = Parsers.sequence(
+        perimeter, colon, floatingRange
+    ).map(GeometryFilterPerimeter::new);
     final Parser<GeometryFilter> geometryFilter = Parsers.or(
         geometryFilterArea,
-        geometryFilterLength);
+        geometryFilterLength,
+        geometryFilterPerimeter);
 
     // changeset id filters
     final Parser<ChangesetIdFilterEquals> 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 <martin.raifer@heigit.org>
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<Void> area = Patterns.string("area").toScanner("area");
     final Parser<Void> length = Patterns.string("length").toScanner("length");
     final Parser<Void> perimeter = Patterns.string("perimeter").toScanner("perimeter");
+    final Parser<Void> vertices = Patterns.string("vertices").toScanner("vertices");
     final Parser<Void> changeset = Patterns.string("changeset").toScanner("changeset");
     final Parser<Void> contributor = Patterns.string("contributor").toScanner("contributor");
 
@@ -207,6 +208,18 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) {
         ),
         Scanners.isChar(')')
     );
+    final Parser<ValueRange> 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<GeometryFilter> geometryFilterArea = Parsers.sequence(
         area, colon, floatingRange
     ).map(GeometryFilterArea::new);
@@ -216,10 +229,14 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) {
     final Parser<GeometryFilter> geometryFilterPerimeter = Parsers.sequence(
         perimeter, colon, floatingRange
     ).map(GeometryFilterPerimeter::new);
+    final Parser<GeometryFilter> geometryFilterVertices = Parsers.sequence(
+        vertices, colon, decimalRange
+    ).map(GeometryFilterVertices::new);
     final Parser<GeometryFilter> geometryFilter = Parsers.or(
         geometryFilterArea,
         geometryFilterLength,
-        geometryFilterPerimeter);
+        geometryFilterPerimeter,
+        geometryFilterVertices);
 
     // changeset id filters
     final Parser<ChangesetIdFilterEquals> 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<Integer, Consumer<Boolean>> 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<Integer, Consumer<Boolean>> 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<Integer, Consumer<Boolean>> 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<Integer, Consumer<Boolean>> 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 <martin.raifer@heigit.org>
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<Void> length = Patterns.string("length").toScanner("length");
     final Parser<Void> perimeter = Patterns.string("perimeter").toScanner("perimeter");
     final Parser<Void> vertices = Patterns.string("vertices").toScanner("vertices");
+    final Parser<Void> outers = Patterns.string("outers").toScanner("outers");
+    final Parser<Void> inners = Patterns.string("inners").toScanner("inners");
     final Parser<Void> changeset = Patterns.string("changeset").toScanner("changeset");
     final Parser<Void> contributor = Patterns.string("contributor").toScanner("contributor");
 
@@ -208,7 +210,7 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) {
         ),
         Scanners.isChar(')')
     );
-    final Parser<ValueRange> decimalRange = Parsers.between(
+    final Parser<ValueRange> 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<GeometryFilter> geometryFilterVertices = Parsers.sequence(
-        vertices, colon, decimalRange
+        vertices, colon, positiveIntegerRange
     ).map(GeometryFilterVertices::new);
+    final Parser<GeometryFilter> geometryFilterOuters = Parsers.sequence(
+        outers, colon, Parsers.or(positiveIntegerRange, number.map(n -> new ValueRange(n, n)))
+    ).map(GeometryFilterOuterRings::new);
+    final Parser<GeometryFilter> geometryFilterInners = Parsers.sequence(
+        inners, colon, Parsers.or(positiveIntegerRange, number.map(n -> new ValueRange(n, n)))
+    ).map(GeometryFilterInnerRings::new);
     final Parser<GeometryFilter> geometryFilter = Parsers.or(
         geometryFilterArea,
         geometryFilterLength,
         geometryFilterPerimeter,
-        geometryFilterVertices);
+        geometryFilterVertices,
+        geometryFilterOuters,
+        geometryFilterInners);
 
     // changeset id filters
     final Parser<ChangesetIdFilterEquals> 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<Integer, Consumer<Boolean>> 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 <martin.raifer@heigit.org>
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<Void> area = Patterns.string("area").toScanner("area");
     final Parser<Void> length = Patterns.string("length").toScanner("length");
     final Parser<Void> perimeter = Patterns.string("perimeter").toScanner("perimeter");
-    final Parser<Void> vertices = Patterns.string("vertices").toScanner("vertices");
-    final Parser<Void> outers = Patterns.string("outers").toScanner("outers");
-    final Parser<Void> inners = Patterns.string("inners").toScanner("inners");
+    final Parser<Void> vertices = Patterns.string("geometry.vertices")
+        .toScanner("geometry.vertices");
+    final Parser<Void> outers = Patterns.string("geometry.outers").toScanner("geometry.outers");
+    final Parser<Void> inners = Patterns.string("geometry.inners").toScanner("geometry.inners");
     final Parser<Void> changeset = Patterns.string("changeset").toScanner("changeset");
     final Parser<Void> 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 <martin.raifer@heigit.org>
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<Void> outers = Patterns.string("geometry.outers").toScanner("geometry.outers");
     final Parser<Void> inners = Patterns.string("geometry.inners").toScanner("geometry.inners");
+    final Parser<Void> roundness = Patterns.string("geometry.roundness")
+        .toScanner("geometry.roundness");
     final Parser<Void> changeset = Patterns.string("changeset").toScanner("changeset");
     final Parser<Void> contributor = Patterns.string("contributor").toScanner("contributor");
 
@@ -241,13 +243,17 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) {
     final Parser<GeometryFilter> geometryFilterInners = Parsers.sequence(
         inners, colon, Parsers.or(positiveIntegerRange, number.map(n -> new ValueRange(n, n)))
     ).map(GeometryFilterInnerRings::new);
+    final Parser<GeometryFilter> geometryFilterRoundness = Parsers.sequence(
+        roundness, colon, floatingRange
+    ).map(GeometryFilterRoundness::new);
     final Parser<GeometryFilter> geometryFilter = Parsers.or(
         geometryFilterArea,
         geometryFilterLength,
         geometryFilterPerimeter,
         geometryFilterVertices,
         geometryFilterOuters,
-        geometryFilterInners);
+        geometryFilterInners,
+        geometryFilterRoundness);
 
     // changeset id filters
     final Parser<ChangesetIdFilterEquals> 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.
+ *
+ * <p>Uses the Polsby-Popper test score, see
+ * <a href="https://en.wikipedia.org/wiki/Polsby%E2%80%93Popper_test">wikipedia</a> for details.</p>
+ */
+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.
+   *
+   * <p>
+   * 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 <a href="https://en.wikipedia.org/wiki/Polsby%E2%80%93Popper_test">wikipedia</a> for info.
+   * </p>
+   *
+   * @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 <martin.raifer@heigit.org>
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<Void> inners = Patterns.string("geometry.inners").toScanner("geometry.inners");
     final Parser<Void> roundness = Patterns.string("geometry.roundness")
         .toScanner("geometry.roundness");
+    final Parser<Void> squareness = Patterns.string("geometry.squareness")
+        .toScanner("geometry.squareness");
     final Parser<Void> changeset = Patterns.string("changeset").toScanner("changeset");
     final Parser<Void> contributor = Patterns.string("contributor").toScanner("contributor");
 
@@ -246,6 +248,9 @@ public FilterParser(TagTranslator tt, boolean allowContributorFilters) {
     final Parser<GeometryFilter> geometryFilterRoundness = Parsers.sequence(
         roundness, colon, floatingRange
     ).map(GeometryFilterRoundness::new);
+    final Parser<GeometryFilter> geometryFilterSquareness = Parsers.sequence(
+        squareness, colon, floatingRange
+    ).map(GeometryFilterSquareness::new);
     final Parser<GeometryFilter> 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<ChangesetIdFilterEquals> 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.
+   *
+   * <p>
+   * Uses an equirectangular distance approximation, which works well assuming segments are short.
+   * </p>
+   *
+   * <p>
+   * Adjusted to partially account for the spheroidal shape of the earth (WGS84 coordinates).
+   * See https://gis.stackexchange.com/a/63047/41632
+   * </p>
+   *
+   * <p>
+   * For typical features present in OpenStreetMap data, the relative error introduced by
+   * this approximation is below 0.1%
+   * </p>
+   *
+   * @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.
+   *
+   * <p>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
+   * </p>
+   *
+   * <p>Adjusted to work directly on geographic coordinates. Implementation works on a few
+   * assumptions: input geometries are "small"; spherical globe approximation.</p>
+   *
+   * @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<LinearRing>(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<LinearRing>();
+      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 <martin.raifer@heigit.org>
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.
+ *
+ * <p>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.</p>
  */
 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 <martin.raifer@heigit.org>
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 <martin.raifer@heigit.org>
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 <martin.raifer@heigit.org>
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 <martin.raifer@heigit.org>
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 <martin.raifer@heigit.org>
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 <martin.raifer@heigit.org>
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 <martin.raifer@heigit.org>
Date: Wed, 2 Nov 2022 17:50:21 +0100
Subject: [PATCH 14/29] Apply suggestions from code review

Co-authored-by: Johannes Visintini <johannes.visintini@heigit.org>
---
 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<GeometryFilter> 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.
    *
    * <p>
-   * 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 <a href="https://en.wikipedia.org/wiki/Polsby%E2%80%93Popper_test">wikipedia</a> for info.
    * </p>
    *
    * @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 <martin.raifer@heigit.org>
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.
-   *
-   * <p>
-   * Uses an equirectangular distance approximation, which works well assuming segments are short.
-   * </p>
-   *
-   * <p>
-   * Adjusted to partially account for the spheroidal shape of the earth (WGS84 coordinates).
-   * See https://gis.stackexchange.com/a/63047/41632
-   * </p>
-   *
-   * <p>
-   * For typical features present in OpenStreetMap data, the relative error introduced by
-   * this approximation is below 0.1%
-   * </p>
-   *
-   * @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.
    *
    * <p>
    * 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.
    *
    * <p>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 <martin.raifer@heigit.org>
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 <martin.raifer@heigit.org>
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<ValueRange> floatingRange = Parsers.between(
+    final Parser<ValueRange> 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<GeometryFilter> geometryFilterArea = Parsers.sequence(
-        area, colon, floatingRange
+        area, colon, positiveFloatingRange
     ).map(GeometryFilterArea::new);
     final Parser<GeometryFilter> geometryFilterLength = Parsers.sequence(
-        length, colon, floatingRange
+        length, colon, positiveFloatingRange
     ).map(GeometryFilterLength::new);
     final Parser<GeometryFilter> geometryFilterPerimeter = Parsers.sequence(
-        perimeter, colon, floatingRange
+        perimeter, colon, positiveFloatingRange
     ).map(GeometryFilterPerimeter::new);
     final Parser<GeometryFilter> 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<GeometryFilter> geometryFilterRoundness = Parsers.sequence(
-        roundness, colon, floatingRange
+        roundness, colon, positiveFloatingRange
     ).map(GeometryFilterRoundness::new);
     final Parser<GeometryFilter> geometryFilterSquareness = Parsers.sequence(
-        squareness, colon, floatingRange
+        squareness, colon, positiveFloatingRange
     ).map(GeometryFilterSquareness::new);
     final Parser<GeometryFilter> geometryFilter = Parsers.or(
         geometryFilterArea,

From b2d99f3d6449dca54358e89f8825b2c133e59ef0 Mon Sep 17 00:00:00 2001
From: Martin Raifer <martin.raifer@heigit.org>
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<Void> whitespace = Scanners.WHITESPACES.skipMany();
 
     final Parser<String> keystr = Patterns.regex("[a-zA-Z_0-9:-]+")

From b8d109700ffc713c72cb00ebf9335b702019c5f3 Mon Sep 17 00:00:00 2001
From: Martin Raifer <martin.raifer@heigit.org>
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 <martin.raifer@heigit.org>
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 <martin.raifer@heigit.org>
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 <martin.raifer@heigit.org>
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 <martin.raifer@heigit.org>
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<Integer, Consumer<Boolean>> 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<Integer, Consumer<Boolean>> 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<Integer, Consumer<Boolean>> 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<Integer, Consumer<Boolean>> 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 <martin.raifer@heigit.org>
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<LinearRing>(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<LinearRing>();
       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<LinearRing> dissolvePolygonToRings(Polygon poly) {
+    var rings = new ArrayList<LinearRing>(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<? extends LineString> 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 <martin.raifer@heigit.org>
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 <martin.raifer@heigit.org>
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<GeometryFilter> geometryFilterArea = Parsers.sequence(
         area, colon, positiveFloatingRange

From c9eb25dc05618eb8bcff15e90431cf2e8c672437 Mon Sep 17 00:00:00 2001
From: Martin Raifer <martin.raifer@heigit.org>
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 <martin.raifer@heigit.org>
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 <martin.raifer@heigit.org>
Date: Thu, 3 Nov 2022 16:44:21 +0100
Subject: [PATCH 29/29] Apply suggestions from code review

Co-authored-by: Johannes Visintini <johannes.visintini@heigit.org>
---
 .../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,