Skip to content

Commit 605d5a3

Browse files
committed
Added support for simplifier monitor
This allows code calling the GeometrySimplifier to be notified of events related to the simplification algorithm. For example, the project geometry-simplifier-debug uses these events to draw images and create videos of the simplification process. And the tests for the simplifier use this to collect data to make further assertions on the results.
1 parent 1791330 commit 605d5a3

8 files changed

+426
-83
lines changed

libs/geo/src/main/java/org/elasticsearch/geometry/simplify/GeometrySimplifier.java

Lines changed: 116 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,17 @@ public abstract class GeometrySimplifier<T extends Geometry> {
2323
protected final int maxPoints;
2424
protected final SimplificationErrorCalculator calculator;
2525
protected final PointError[] points;
26+
protected final Monitor monitor;
2627
protected int length;
28+
protected String description;
2729

2830
protected final PriorityQueue<PointError> queue = new PriorityQueue<>();
2931

30-
public GeometrySimplifier(int maxPoints, SimplificationErrorCalculator calculator) {
32+
protected GeometrySimplifier(String description, int maxPoints, SimplificationErrorCalculator calculator, Monitor monitor) {
33+
this.description = description;
3134
this.maxPoints = maxPoints;
3235
this.calculator = calculator;
36+
this.monitor = monitor;
3337
this.points = new PointError[maxPoints];
3438
this.length = 0;
3539
}
@@ -61,9 +65,11 @@ public void consume(double x, double y) {
6165
// Remove point with lowest error
6266
PointError toRemove = queue.remove();
6367
removeAndAdd(toRemove.index, pointError);
68+
notifyMonitorPointRemoved(toRemove);
6469
} else {
6570
this.points[length] = pointError;
6671
length++;
72+
notifyMonitorPointAdded();
6773
}
6874
}
6975

@@ -134,22 +140,100 @@ public String toString() {
134140
}
135141

136142
@Override
137-
public double getX() {
143+
public double x() {
138144
return x;
139145
}
140146

141147
@Override
142-
public double getY() {
148+
public double y() {
143149
return y;
144150
}
145151
}
146152

153+
/**
154+
* Implementation of this interface will receive calls with internal data at each step of the
155+
* simplification algorithm. This is of use for debugging complex cases, as well as gaining insight
156+
* into the way the algorithm works. Data provided in the callback includes:
157+
* <ul>
158+
* <li>String description of current process</li>
159+
* <li>List of points in current simplification</li>
160+
* <li>Last point removed from the simplification</li>
161+
* </ul>
162+
* mode, list of points representing the current linked-list of internal nodes used for
163+
* triangulation, and a list of triangles so far created by the algorithm.
164+
*/
165+
public interface Monitor {
166+
/** Every time a point is added to the collection, this method sends the resulting state */
167+
void pointAdded(String status, List<SimplificationErrorCalculator.PointLike> points);
168+
169+
/** Every time a point is added and another is removed from the collection, this method sends the resulting state */
170+
void pointRemoved(
171+
String status,
172+
List<SimplificationErrorCalculator.PointLike> points,
173+
SimplificationErrorCalculator.PointLike removed,
174+
double error,
175+
SimplificationErrorCalculator.PointLike previous,
176+
SimplificationErrorCalculator.PointLike next
177+
);
178+
179+
/**
180+
* When a new simplification or sub-simplification starts, this provides a description of the simplification,
181+
* as well as the current maxPoints target for this simplification. For a single simplification, maxPoints
182+
* will simply be the value passed to the constructor, but compound simplifications will calculate smaller
183+
* numbers for sub-simplifications (eg. holes in polygons, or shells in multi-polygons).
184+
*/
185+
void startSimplification(String description, int maxPoints);
186+
187+
/**
188+
* When simplification or sub-simplification is completed, this is called.
189+
*/
190+
void endSimplification(String description, List<SimplificationErrorCalculator.PointLike> points);
191+
}
192+
193+
protected void notifyMonitorSimplificationStart() {
194+
if (monitor != null) {
195+
monitor.startSimplification(description, maxPoints);
196+
}
197+
}
198+
199+
protected void notifyMonitorSimplificationEnd() {
200+
if (monitor != null) {
201+
monitor.endSimplification(description, getCurrentPoints());
202+
}
203+
}
204+
205+
protected void notifyMonitorPointRemoved(PointError removed) {
206+
if (monitor != null) {
207+
PointError previous = points[removed.index - 1];
208+
PointError next = points[removed.index];
209+
monitor.pointRemoved(description + ".addAndRemovePoint()", getCurrentPoints(), removed, removed.error, previous, next);
210+
}
211+
}
212+
213+
protected void notifyMonitorPointAdded() {
214+
if (monitor != null) {
215+
monitor.pointAdded(description + ".addPoint()", getCurrentPoints());
216+
}
217+
}
218+
219+
private List<SimplificationErrorCalculator.PointLike> getCurrentPoints() {
220+
ArrayList<SimplificationErrorCalculator.PointLike> simplification = new ArrayList<>();
221+
for (int i = 0; i < length; i++) {
222+
simplification.add(points[i]);
223+
}
224+
return simplification;
225+
}
226+
147227
/**
148228
* Simplifies a Line geometry to the specified maximum number of points.
149229
*/
150230
public static class LineStrings extends GeometrySimplifier<Line> {
151231
public LineStrings(int maxPoints, SimplificationErrorCalculator calculator) {
152-
super(maxPoints, calculator);
232+
this(maxPoints, calculator, null);
233+
}
234+
235+
public LineStrings(int maxPoints, SimplificationErrorCalculator calculator, Monitor monitor) {
236+
super("LineString", maxPoints, calculator, monitor);
153237
}
154238

155239
@Override
@@ -158,9 +242,11 @@ public Line simplify(Line line) {
158242
return line;
159243
}
160244
reset();
245+
notifyMonitorSimplificationStart();
161246
for (int i = 0; i < line.length(); i++) {
162247
consume(line.getX(i), line.getY(i));
163248
}
249+
notifyMonitorSimplificationEnd();
164250
return produce();
165251
}
166252

@@ -185,7 +271,11 @@ public Line produce() {
185271
*/
186272
public static class LinearRings extends GeometrySimplifier<LinearRing> {
187273
public LinearRings(int maxPoints, SimplificationErrorCalculator calculator) {
188-
super(maxPoints, calculator);
274+
this(maxPoints, calculator, null);
275+
}
276+
277+
public LinearRings(int maxPoints, SimplificationErrorCalculator calculator, Monitor monitor) {
278+
super("LinearRing", maxPoints, calculator, monitor);
189279
assert maxPoints >= 4;
190280
}
191281

@@ -195,9 +285,11 @@ public LinearRing simplify(LinearRing ring) {
195285
return ring;
196286
}
197287
reset();
288+
notifyMonitorSimplificationStart();
198289
for (int i = 0; i < ring.length(); i++) {
199290
consume(ring.getX(i), ring.getY(i));
200291
}
292+
notifyMonitorSimplificationEnd();
201293
return produce();
202294
}
203295

@@ -221,7 +313,11 @@ public static class Polygons extends GeometrySimplifier<Polygon> {
221313
ArrayList<GeometrySimplifier<LinearRing>> holeSimplifiers = new ArrayList<>();
222314

223315
public Polygons(int maxPoints, SimplificationErrorCalculator calculator) {
224-
super(maxPoints, calculator);
316+
this(maxPoints, calculator, null);
317+
}
318+
319+
public Polygons(int maxPoints, SimplificationErrorCalculator calculator, Monitor monitor) {
320+
super("Polygon", maxPoints, calculator, monitor);
225321
}
226322

227323
@Override
@@ -237,17 +333,20 @@ public Polygon simplify(Polygon geometry) {
237333
return geometry;
238334
}
239335
reset();
336+
notifyMonitorSimplificationStart();
240337
for (int i = 0; i < ring.length(); i++) {
241338
consume(ring.getX(i), ring.getY(i));
242339
}
243340
for (int i = 0; i < geometry.getNumberOfHoles(); i++) {
244341
LinearRing hole = geometry.getHole(i);
245342
double simplificationFactor = (double) maxPoints / ring.length();
246343
int maxHolePoints = Math.max(4, (int) (simplificationFactor * hole.length()));
247-
LinearRings holeSimplifier = new LinearRings(maxHolePoints, calculator);
344+
LinearRings holeSimplifier = new LinearRings(maxHolePoints, calculator, this.monitor);
345+
holeSimplifier.description = "Polygon.Hole";
248346
holeSimplifiers.add(holeSimplifier);
249347
holeSimplifier.simplify(hole);
250348
}
349+
notifyMonitorSimplificationEnd();
251350
return produce();
252351
}
253352

@@ -267,7 +366,7 @@ private List<LinearRing> produceHoles() {
267366
* It does not make use of its own simplifier capabilities.
268367
* The largest inner polygon is simplified to the specified maxPoints, while the rest are simplified
269368
* to a maxPoints value that is a fraction of their size compared to the largest size.
270-
*
369+
* <p>
271370
* Note that this simplifier cannot work in streaming mode.
272371
* Since a MultiPolygon can contain more than one polygon,
273372
* the <code>consume(Point)</code> method would not know which polygon to add to.
@@ -279,7 +378,11 @@ public static class MultiPolygons extends GeometrySimplifier<MultiPolygon> {
279378
ArrayList<Integer> indexes = new ArrayList<>();
280379

281380
public MultiPolygons(int maxPoints, SimplificationErrorCalculator calculator) {
282-
super(maxPoints, calculator);
381+
this(maxPoints, calculator, null);
382+
}
383+
384+
public MultiPolygons(int maxPoints, SimplificationErrorCalculator calculator, Monitor monitor) {
385+
super("MultiPolygon", maxPoints, calculator, monitor);
283386
}
284387

285388
@Override
@@ -296,18 +399,21 @@ public MultiPolygon simplify(MultiPolygon geometry) {
296399
Polygon polygon = geometry.get(i);
297400
maxPolyLength = Math.max(maxPolyLength, polygon.getPolygon().length());
298401
}
402+
notifyMonitorSimplificationStart();
299403
for (int i = 0; i < geometry.size(); i++) {
300404
Polygon polygon = geometry.get(i);
301405
double simplificationFactor = (double) maxPoints / maxPolyLength;
302406
int maxPolyPoints = Math.max(4, (int) (simplificationFactor * polygon.getPolygon().length()));
303-
Polygons simplifier = new Polygons(maxPolyPoints, calculator);
407+
Polygons simplifier = new Polygons(maxPolyPoints, calculator, monitor);
408+
simplifier.description = "MultiPolygon.Polygon[" + i + "]";
304409
simplifier.simplify(polygon);
305410
if (simplifier.length > 0) {
306411
// Invalid polygons (all points co-located) will not be simplified
307412
polygonSimplifiers.add(simplifier);
308413
indexes.add(i);
309414
}
310415
}
416+
notifyMonitorSimplificationEnd();
311417
return produce();
312418
}
313419

libs/geo/src/main/java/org/elasticsearch/geometry/simplify/SimplificationErrorCalculator.java

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,16 @@ public interface SimplificationErrorCalculator {
1414
double calculateError(PointLike left, PointLike middle, PointLike right);
1515

1616
interface PointLike {
17-
double getX();
17+
double x();
1818

19-
double getY();
19+
double y();
2020
}
2121

2222
static SimplificationErrorCalculator byName(String calculatorName) {
2323
return switch (calculatorName.toLowerCase(Locale.ROOT)) {
2424
case "cartesiantrianglearea" -> new CartesianTriangleAreaCalculator();
2525
case "trianglearea" -> new TriangleAreaCalculator();
26+
case "triangleheight" -> new TriangleHeightCalculator();
2627
case "frecheterror" -> new FrechetErrorCalculator();
2728
default -> throw new IllegalArgumentException("Unknown geometry simplification error calculator: " + calculatorName);
2829
};
@@ -35,10 +36,10 @@ class CartesianTriangleAreaCalculator implements SimplificationErrorCalculator {
3536

3637
@Override
3738
public double calculateError(PointLike left, PointLike middle, PointLike right) {
38-
double xb = middle.getX() - left.getX();
39-
double yb = middle.getY() - left.getY();
40-
double xc = right.getX() - left.getX();
41-
double yc = right.getY() - left.getY();
39+
double xb = middle.x() - left.x();
40+
double yb = middle.y() - left.y();
41+
double xc = right.x() - left.x();
42+
double yc = right.y() - left.y();
4243
return 0.5 * Math.abs(xb * yc - xc * yb);
4344
}
4445
}
@@ -70,7 +71,39 @@ public double calculateError(PointLike left, PointLike middle, PointLike right)
7071
}
7172

7273
private double distance(PointLike a, PointLike b) {
73-
return SloppyMath.haversinMeters(a.getY(), a.getX(), b.getY(), b.getX());
74+
return SloppyMath.haversinMeters(a.y(), a.x(), b.y(), b.x());
75+
}
76+
}
77+
78+
/**
79+
* Calculate the triangle area using geographic coordinates and Herons formula (side lengths)
80+
* as described at https://en.wikipedia.org/wiki/Area_of_a_triangle, but scale the area down
81+
* by the inverse of the length of the base (left-right), which estimates the height of the triangle.
82+
*/
83+
class TriangleHeightCalculator implements SimplificationErrorCalculator {
84+
85+
@Override
86+
public double calculateError(PointLike left, PointLike middle, PointLike right) {
87+
// Calculate side lengths using approximate haversine
88+
double a = distance(left, right);
89+
double b = distance(right, middle);
90+
double c = distance(middle, left);
91+
// semi-perimeter
92+
double s = 0.5 * (a + b + c); // Semi-perimeter
93+
double da = s - a;
94+
double db = s - b;
95+
double dc = s - c;
96+
if (da >= 0 && db >= 0 && dc >= 0) {
97+
// Herons formula, scaled by 1/a
98+
return 2.0 * Math.sqrt(s * da * db * dc) / a;
99+
} else {
100+
// rounding errors can cause flat triangles to have negative values, leading to NaN areas
101+
return 0.0;
102+
}
103+
}
104+
105+
private double distance(PointLike a, PointLike b) {
106+
return SloppyMath.haversinMeters(a.y(), a.x(), b.y(), b.x());
74107
}
75108
}
76109

@@ -79,15 +112,15 @@ class FrechetErrorCalculator implements SimplificationErrorCalculator {
79112
@Override
80113
public double calculateError(PointLike left, PointLike middle, PointLike right) {
81114
// Offset coordinates so left is at the origin
82-
double rightX = right.getX() - left.getX();
83-
double rightY = right.getY() - left.getY();
115+
double rightX = right.x() - left.x();
116+
double rightY = right.y() - left.y();
84117
if (Math.abs(rightX) > 1e-10 || Math.abs(rightY) > 1e-10) {
85118
// Rotate coordinates so that left->right is horizontal
86119
double len = Math.sqrt(rightX * rightX + rightY * rightY);
87120
double cos = rightX / len;
88121
double sin = rightY / len;
89-
double middleX = middle.getX() - left.getX();
90-
double middleY = middle.getY() - left.getY();
122+
double middleX = middle.x() - left.x();
123+
double middleY = middle.y() - left.y();
91124
double middleXrotated = middleX * cos + middleY * sin;
92125
double middleYrotated = middleY * cos - middleX * sin;
93126
double rightXrotated = rightX * cos + rightY * sin;

libs/geo/src/test/java/org/elasticsearch/geometry/simplify/GeometrySimplifierCartesianTriangleAreaTests.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,19 @@
88

99
package org.elasticsearch.geometry.simplify;
1010

11+
import org.elasticsearch.geometry.Line;
12+
1113
public class GeometrySimplifierCartesianTriangleAreaTests extends GeometrySimplifierTests {
1214
private SimplificationErrorCalculator calculator = new SimplificationErrorCalculator.CartesianTriangleAreaCalculator();
1315

1416
@Override
1517
protected SimplificationErrorCalculator calculator() {
1618
return calculator;
1719
}
20+
21+
protected void assertLineWithNarrowSpikes(Line simplified, int spikeCount) {
22+
// The cartesian triangle area destroys most spikes, saving only two
23+
assertLineSpikes("Cartesian", simplified, 2);
24+
}
25+
1826
}

libs/geo/src/test/java/org/elasticsearch/geometry/simplify/GeometrySimplifierFrechetTests.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,18 @@
88

99
package org.elasticsearch.geometry.simplify;
1010

11+
import org.elasticsearch.geometry.Line;
12+
1113
public class GeometrySimplifierFrechetTests extends GeometrySimplifierTests {
1214
private SimplificationErrorCalculator calculator = new SimplificationErrorCalculator.FrechetErrorCalculator();
1315

1416
@Override
1517
protected SimplificationErrorCalculator calculator() {
1618
return calculator;
1719
}
20+
21+
protected void assertLineWithNarrowSpikes(Line simplified, int spikeCount) {
22+
// The frechet distance can save all spikes
23+
assertLineSpikes("Frechet", simplified, spikeCount);
24+
}
1825
}

0 commit comments

Comments
 (0)