diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java
index 199277dafa..5186c624a2 100644
a
|
b
|
public class Geometry {
|
44 | 44 | Path2D.Float line = null; |
45 | 45 | Area area = null; |
46 | 46 | // MVT uses delta encoding. Each feature starts at (0, 0). |
47 | | double x = 0; |
48 | | double y = 0; |
| 47 | int x = 0; |
| 48 | int y = 0; |
49 | 49 | // Area is used to determine the inner/outer of a polygon |
50 | | double areaAreaSq = 0; |
| 50 | final int maxArraySize = commands.stream().filter(command -> command.getType() != Command.ClosePath).mapToInt(command -> command.getOperations().length).sum(); |
| 51 | final List<Integer> xArray = new ArrayList<>(maxArraySize); |
| 52 | final List<Integer> yArray = new ArrayList<>(maxArraySize); |
51 | 53 | for (CommandInteger command : commands) { |
52 | 54 | final short[] operations = command.getOperations(); |
53 | 55 | // Technically, there is no reason why there can be multiple MoveTo operations in one command, but that is undefined behavior |
54 | 56 | if (command.getType() == Command.MoveTo && operations.length == 2) { |
55 | | areaAreaSq = 0; |
56 | 57 | x += operations[0]; |
57 | 58 | y += operations[1]; |
58 | 59 | line = new Path2D.Float(); |
59 | 60 | line.moveTo(x, y); |
| 61 | xArray.add(x); |
| 62 | yArray.add(y); |
60 | 63 | shapes.add(line); |
61 | 64 | } else if (command.getType() == Command.LineTo && operations.length % 2 == 0 && line != null) { |
62 | 65 | for (int i = 0; i < operations.length / 2; i++) { |
63 | | final double lx = x; |
64 | | final double ly = y; |
65 | 66 | x += operations[2 * i]; |
66 | 67 | y += operations[2 * i + 1]; |
67 | | areaAreaSq += lx * y - x * ly; |
| 68 | xArray.add(x); |
| 69 | yArray.add(y); |
68 | 70 | line.lineTo(x, y); |
69 | 71 | } |
70 | 72 | // ClosePath should only be used with Polygon geometry |
… |
… |
public class Geometry {
|
76 | 78 | shapes.add(area); |
77 | 79 | } |
78 | 80 | |
| 81 | final double areaAreaSq = calculateSurveyorsArea(xArray.stream().mapToInt(i -> i).toArray(), yArray.stream().mapToInt(i -> i).toArray()); |
79 | 82 | Area nArea = new Area(line); |
80 | 83 | // SonarLint thinks that this is never > 0. It can be. |
81 | 84 | if (areaAreaSq > 0) { |
… |
… |
public class Geometry {
|
92 | 95 | } |
93 | 96 | } |
94 | 97 | |
| 98 | /** |
| 99 | * This is also known as the "shoelace formula". |
| 100 | * @param xArray The array of x coordinates |
| 101 | * @param yArray The array of y coordinates |
| 102 | * @return The area of the object |
| 103 | * @throws IllegalArgumentException if the array lengths are not equal |
| 104 | */ |
| 105 | static double calculateSurveyorsArea(int[] xArray, int[] yArray) { |
| 106 | if (xArray.length != yArray.length) { |
| 107 | throw new IllegalArgumentException("Cannot calculate areas when arrays are uneven"); |
| 108 | } |
| 109 | // Lines have no area |
| 110 | if (xArray.length < 3) { |
| 111 | return 0; |
| 112 | } |
| 113 | int area = 0; |
| 114 | // Do the non-special stuff first (x0 * y1 - x1 * y0) |
| 115 | for (int i = 0; i < xArray.length - 1; i++) { |
| 116 | area += xArray[i] * yArray[i + 1] - xArray[i + 1] * yArray[i]; |
| 117 | } |
| 118 | // Now calculate the edges (xn * y0 - x0 * yn) |
| 119 | area += xArray[xArray.length - 1] * yArray[0] - xArray[0] * yArray[yArray.length - 1]; |
| 120 | return area / 2d; |
| 121 | } |
| 122 | |
95 | 123 | /** |
96 | 124 | * Get the shapes to draw this geometry with |
97 | 125 | * @return A collection of shapes |
diff --git a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTest.java b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTest.java
index 14731168f7..0c6e2c1ae9 100644
a
|
b
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
6 | 6 | import static org.junit.jupiter.api.Assertions.assertThrows; |
7 | 7 | import static org.junit.jupiter.api.Assertions.assertTrue; |
8 | 8 | |
9 | | |
10 | 9 | import java.awt.geom.Area; |
11 | 10 | import java.awt.geom.Ellipse2D; |
12 | 11 | import java.awt.geom.Path2D; |
… |
… |
class GeometryTest {
|
138 | 137 | assertEquals("POLYGON cannot have zero area", exception.getMessage()); |
139 | 138 | } |
140 | 139 | |
| 140 | /** |
| 141 | * This checks that the area is properly calculated |
| 142 | */ |
| 143 | @Test |
| 144 | void testNonRegression20971And21254() { |
| 145 | assertEquals(15.0, Geometry.calculateSurveyorsArea(new int[]{1507, 1509, 1509}, new int[]{3029, 3018, 3033})); |
| 146 | assertEquals(0.0, Geometry.calculateSurveyorsArea(new int[]{0, 0, 0}, new int[]{0, 0, 0})); |
| 147 | assertEquals(0.0, Geometry.calculateSurveyorsArea(new int[2], new int[2])); |
| 148 | assertThrows(IllegalArgumentException.class, () -> Geometry.calculateSurveyorsArea(new int[3], new int[4])); |
| 149 | } |
| 150 | |
141 | 151 | @Test |
142 | 152 | void testMultiPolygon() { |
143 | 153 | List<CommandInteger> commands = new ArrayList<>(10); |