Skip to content

Commit b290089

Browse files
committed
test: refactorings and tests + a lot of small bugs fixed
1 parent dab0ed4 commit b290089

File tree

19 files changed

+800
-177
lines changed

19 files changed

+800
-177
lines changed

freepapermaps/src/main/java/io/github/mrmaxguns/freepapermaps/geometry/BaseCoordinate.java

Lines changed: 0 additions & 81 deletions
This file was deleted.

freepapermaps/src/main/java/io/github/mrmaxguns/freepapermaps/geometry/BoundingBox.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* BoundingBox represents a box in space defined as the rectangular region between a top left point and a bottom right
66
* point. BoundingBoxes are immutable, just like Coordinates.
77
*/
8-
public class BoundingBox<C extends BaseCoordinate<C>> {
8+
public class BoundingBox<C extends Coordinate<?>> {
99
/** The top left corner (minimum x/lon, maximum y/lat). */
1010
private final C topLeftCorner;
1111
/** The bottom right corner (maximum x/lon, minimum y/lat). */
@@ -56,28 +56,40 @@ public double getHeight() {
5656

5757
public double getMaxLat() { return getMaxY(); }
5858

59+
@Override
60+
public boolean equals(Object other) {
61+
if (!(other instanceof BoundingBox<?>)) { return false; }
62+
return equals((BoundingBox<?>) other);
63+
}
64+
5965
/** Returns true if the top left and bottom right coordinates of both bounding boxes are exactly equal. */
60-
public boolean equals(BoundingBox<C> other) {
66+
public boolean equals(BoundingBox<?> other) {
6167
return equals(other, 0.0);
6268
}
6369

6470
/**
6571
* Returns true if the top left and bottom right coordinates of both bounding boxes are within (inclusive) of some
6672
* <code>epsilon</code>.
6773
*/
68-
public boolean equals(BoundingBox<C> other, double epsilon) {
74+
public boolean equals(BoundingBox<?> other, double epsilon) {
6975
return equals(other, epsilon, epsilon);
7076
}
7177

7278
/**
7379
* Returns true if the top left and bottom right coordinates of both bounding boxes are within (inclusive) of some
7480
* x and y epsilon.
7581
*/
76-
public boolean equals(BoundingBox<C> other, double epsilonX, double epsilonY) {
82+
public boolean equals(BoundingBox<?> other, double epsilonX, double epsilonY) {
7783
return topLeftCorner.equals(other.getTopLeftCorner(), epsilonX, epsilonY) &&
7884
bottomRightCorner.equals(other.getBottomRightCorner(), epsilonX, epsilonY);
7985
}
8086

87+
/** Returns true if both the top left and bottom right coordinates of both bounding boxes are almost equal. */
88+
public boolean almostEquals(BoundingBox<?> other) {
89+
return topLeftCorner.almostEquals(other.getTopLeftCorner()) &&
90+
bottomRightCorner.almostEquals(other.getBottomRightCorner());
91+
}
92+
8193
@Override
8294
public String toString() {
8395
return "BoundingBox(topLeftCorner=" + topLeftCorner + ", bottomRightCorner=" + bottomRightCorner + ")";
Lines changed: 100 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,106 @@
11
package io.github.mrmaxguns.freepapermaps.geometry;
22

3-
public interface Coordinate {
4-
double getX();
3+
/** Coordinate represents an x/y or longitude/latitude position in space. Coordinates are immutable. */
4+
public abstract class Coordinate<T extends Coordinate<T>> {
5+
/** Represents x or longitude. */
6+
private final double x;
7+
/** Represents y or latitude. */
8+
private final double y;
59

6-
double getY();
10+
public Coordinate(double xOrLon, double yOrLat) {
11+
x = xOrLon;
12+
y = yOrLat;
13+
}
714

8-
double getLon();
15+
/**
16+
* Creates an instance of a Coordinate subclass. This is needed so that methods defined in the abstract
17+
* Coordinate class can create instances of the subclass.
18+
*/
19+
protected abstract T createInstance(double xOrLon, double yOrLat);
920

10-
double getLat();
21+
public double getX() {
22+
return x;
23+
}
24+
25+
public double getY() {
26+
return y;
27+
}
28+
29+
public double getLon() {
30+
return x;
31+
}
32+
33+
public double getLat() {
34+
return y;
35+
}
36+
37+
/** Returns a new Coordinate which is the sum of this one and <code>other</code>. */
38+
public T add(T other) {
39+
return createInstance(x + other.getX(), y + other.getY());
40+
}
41+
42+
/** Returns a new Coordinate which is the result of this coordinate minus <code>other</code>. */
43+
public T subtract(T other) {
44+
return createInstance(x - other.getX(), y - other.getY());
45+
}
46+
47+
/** Returns a new Coordinate whose components are multiplied by <code>scaleFactor</code>. */
48+
public T scale(double scaleFactor) {
49+
return createInstance(x * scaleFactor, y * scaleFactor);
50+
}
51+
52+
@Override
53+
public boolean equals(Object other) {
54+
if (!(other instanceof Coordinate<?>)) { return false; }
55+
return equals((Coordinate<?>) other);
56+
}
57+
58+
/** Returns true if both coordinates are exactly equal. */
59+
public boolean equals(Coordinate<?> other) {
60+
return equals(other, 0.0);
61+
}
62+
63+
/** Returns true if both coordinate x and y values are within (inclusive) of some <code>epsilon</code>. */
64+
public boolean equals(Coordinate<?> other, double epsilon) {
65+
return equals(other, epsilon, epsilon);
66+
}
67+
68+
/** Returns true if both coordinate x and y values are within (inclusive) of some x and y epsilon, respectively. */
69+
public boolean equals(Coordinate<?> other, double epsilonX, double epsilonY) {
70+
if (this == other) {
71+
return true;
72+
}
73+
return (Math.abs(x - other.getX()) <= epsilonX) && (Math.abs(y - other.getY()) <= epsilonY);
74+
}
75+
76+
/**
77+
* Returns true if both coordinate x and y values are almost equal for the purposes of geography, regardless of
78+
* their magnitude.
79+
*/
80+
public boolean almostEquals(Coordinate<?> other) {
81+
return valuesAlmostEqual(x, other.getX()) && valuesAlmostEqual(y, other.getY());
82+
}
83+
84+
/** Returns true if two doubles are almost equal, given hard-coded minimum and relative tolerances. */
85+
private boolean valuesAlmostEqual(double a, double b) {
86+
final double absEpsilon = 1e-12; // minimum absolute threshold
87+
final double relEpsilon = 1e-9; // relative tolerance
88+
89+
double diff = Math.abs(a - b);
90+
double largest = Math.max(Math.abs(a), Math.abs(b));
91+
92+
return diff <= Math.max(absEpsilon, largest * relEpsilon);
93+
}
94+
95+
/** Returns a short, human-readable identifier of the Coordinate's type (e.g. WGS84). */
96+
public abstract String getCoordinateType();
97+
98+
/** Returns a string representing an appropriate unit for this coordinate (e.g. m). */
99+
public abstract String getCoordinateUnit();
100+
101+
/** Returns a String representation of this Coordinate. */
102+
@Override
103+
public String toString() {
104+
return getCoordinateType() + "(" + x + getCoordinateUnit() + ", " + y + getCoordinateUnit() + ")";
105+
}
11106
}

freepapermaps/src/main/java/io/github/mrmaxguns/freepapermaps/geometry/Geometry.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,22 @@
33
import io.github.mrmaxguns.freepapermaps.osm.TagList;
44

55

6+
/** Represents a geometry loosely based on both the OSM and SimpleFeatures specifications. */
67
public abstract class Geometry {
8+
/** A list of OSM tags associated with this <code>Geometry</code>. */
79
private final TagList tags = new TagList();
810

911
public TagList getTags() {
1012
return tags;
1113
}
1214

15+
/** Returns true if the <code>Geometry</code> passes basic validation checks that are essential for proper
16+
* rendering. */
1317
public abstract boolean isValid();
18+
19+
/**
20+
* Returns true if the <code>Geometry</code> passes more computationally expensive validation checks that may not
21+
* be necessary for rendering and/or handle rare edge cases.
22+
*/
23+
public abstract boolean isCompletelyValid();
1424
}

freepapermaps/src/main/java/io/github/mrmaxguns/freepapermaps/geometry/LineSegment.java

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
11
package io.github.mrmaxguns.freepapermaps.geometry;
22

33

4-
public record LineSegment(Coordinate edge1, Coordinate edge2) {
5-
// Based on https://stackoverflow.com/a/3842240/13501679 and https://www.geeksforgeeks
6-
// .org/dsa/check-if-two-given-line-segments-intersect/
4+
import io.github.mrmaxguns.freepapermaps.projections.RawCoordinate;
5+
6+
7+
public record LineSegment(Coordinate<?> edge1, Coordinate<?> edge2) {
8+
// Based on https://stackoverflow.com/a/3842240/13501679 and
9+
// https://www.geeksforgeeks.org/dsa/check-if-two-given-line-segments-intersect/
710
public boolean intersects(LineSegment other) {
811
Orientation o1 = getOrientation(edge1, edge2, other.edge1);
912
Orientation o2 = getOrientation(edge1, edge2, other.edge2);
1013
Orientation o3 = getOrientation(other.edge1, other.edge2, edge1);
1114
Orientation o4 = getOrientation(other.edge1, other.edge2, edge2);
1215

1316
if (o1 != o2 && o3 != o4) {
14-
return false;
17+
// Calculate intersection point, since we don't count simply sharing the same node as an intersection.
18+
Coordinate<?> intersection = computeIntersectionPoint(this.edge1, this.edge2, other.edge1, other.edge2);
19+
20+
if (intersection == null) {
21+
// The intersection should only be null if the lines are parallel, in which case the original condition
22+
// of this if-statement should not have been triggered.
23+
throw new RuntimeException("Failed to determine whether line segments intersect.");
24+
}
25+
26+
// If intersection is exactly an endpoint, ignore it.
27+
return !intersection.equals(edge1) && !intersection.equals(edge2) && !intersection.equals(other.edge1) &&
28+
!intersection.equals(other.edge2); // touching at endpoint only
1529
}
1630

1731
// Special cases
@@ -32,12 +46,12 @@ public boolean intersects(LineSegment other) {
3246
}
3347

3448
// Returns true if q is on the segment between p and r.
35-
private boolean onSegment(Coordinate p, Coordinate q, Coordinate r) {
49+
private boolean onSegment(Coordinate<?> p, Coordinate<?> q, Coordinate<?> r) {
3650
return (q.getX() <= Math.max(p.getX(), r.getX()) && q.getX() >= Math.min(p.getX(), r.getX()) &&
3751
q.getY() <= Math.max(p.getY(), r.getY()) && q.getY() >= Math.min(p.getY(), r.getY()));
3852
}
3953

40-
private Orientation getOrientation(Coordinate p, Coordinate q, Coordinate r) {
54+
private Orientation getOrientation(Coordinate<?> p, Coordinate<?> q, Coordinate<?> r) {
4155
double t1 = (q.getY() - p.getY()) * (r.getX() - q.getX());
4256
double t2 = (q.getX() - p.getX()) * (r.getY() - q.getY());
4357
double determinant = t1 - t2;
@@ -53,5 +67,31 @@ private Orientation getOrientation(Coordinate p, Coordinate q, Coordinate r) {
5367
return determinant > 0 ? Orientation.Clockwise : Orientation.Counterclockwise;
5468
}
5569

70+
public boolean isDegenerate() {
71+
return edge1.equals(edge2);
72+
}
73+
5674
private enum Orientation {Collinear, Clockwise, Counterclockwise}
75+
76+
private Coordinate<?> computeIntersectionPoint(Coordinate<?> p1, Coordinate<?> q1, Coordinate<?> p2,
77+
Coordinate<?> q2) {
78+
double A1 = q1.getY() - p1.getY();
79+
double B1 = p1.getX() - q1.getX();
80+
double C1 = A1 * p1.getX() + B1 * p1.getY();
81+
82+
double A2 = q2.getY() - p2.getY();
83+
double B2 = p2.getX() - q2.getX();
84+
double C2 = A2 * p2.getX() + B2 * p2.getY();
85+
86+
double det = A1 * B2 - A2 * B1;
87+
88+
if (det == 0) {
89+
// Lines are parallel
90+
return null;
91+
} else {
92+
double x = (B2 * C1 - B1 * C2) / det;
93+
double y = (A1 * C2 - A2 * C1) / det;
94+
return new RawCoordinate(x, y);
95+
}
96+
}
5797
}

0 commit comments

Comments
 (0)