Skip to content

Commit b6d687f

Browse files
authored
fix(java): Fix handling of geometrycollection and empty geometries (#1508)
Features with empty geometry still got an ID assigned, leading to a mismatch on decoding. The geometry collection type wasn't handled at all.
1 parent 9fcae58 commit b6d687f

3 files changed

Lines changed: 304 additions & 1 deletion

File tree

java/mlt-core/src/main/java/org/maplibre/mlt/converter/MltConverter.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -622,7 +622,8 @@ public static <T extends OutputStream> T encode(
622622
throw new RuntimeException("Missing Metadata");
623623
}
624624

625-
final var sourceFeatures = sourceLayer.features();
625+
final var sourceFeatures =
626+
sourceLayer.features().stream().filter(GeometryEncoder::hasGeometry).toList();
626627
if (sourceFeatures.isEmpty()) {
627628
continue;
628629
}

java/mlt-core/src/main/java/org/maplibre/mlt/converter/encodings/GeometryEncoder.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.apache.commons.lang3.tuple.Pair;
2424
import org.jetbrains.annotations.NotNull;
2525
import org.locationtech.jts.geom.Geometry;
26+
import org.locationtech.jts.geom.GeometryCollection;
2627
import org.locationtech.jts.geom.GeometryFactory;
2728
import org.locationtech.jts.geom.LineString;
2829
import org.locationtech.jts.geom.LinearRing;
@@ -39,6 +40,7 @@
3940
import org.maplibre.mlt.converter.geometry.Vertex;
4041
import org.maplibre.mlt.converter.geometry.ZOrderCurve;
4142
import org.maplibre.mlt.converter.tessellation.TessellationUtils;
43+
import org.maplibre.mlt.data.Feature;
4244
import org.maplibre.mlt.metadata.stream.DictionaryType;
4345
import org.maplibre.mlt.metadata.stream.LengthType;
4446
import org.maplibre.mlt.metadata.stream.LogicalLevelTechnique;
@@ -364,6 +366,23 @@ private static void prepareGeometry(
364366
vertexBuffer.add(new Vertex((int) point.getX(), (int) point.getY()));
365367
}
366368
}
369+
case GeometryCollection geometryCollection -> {
370+
final var nestedGeometries =
371+
IntStream.range(0, geometryCollection.getNumGeometries())
372+
.mapToObj(geometryCollection::getGeometryN)
373+
.filter(GeometryEncoder::hasGeometry)
374+
.toList();
375+
prepareGeometry(
376+
nestedGeometries,
377+
numGeometries,
378+
geometryTypes,
379+
vertexBuffer,
380+
numParts,
381+
numRings,
382+
numTriangles,
383+
indexBuffer,
384+
tessellateSource);
385+
}
367386
default ->
368387
throw new IllegalArgumentException(
369388
"Specified geometry type is not (yet) supported: " + geometry.getGeometryType());
@@ -634,4 +653,24 @@ private static ArrayList<byte[]> encodeVertexBuffer(
634653
result.add(encodedValues);
635654
return result;
636655
}
656+
657+
public static boolean hasGeometry(@Nullable Feature feature) {
658+
return (feature != null) && hasGeometry(feature.getGeometry());
659+
}
660+
661+
public static boolean hasGeometry(@Nullable Geometry geometry) {
662+
return switch (geometry) {
663+
case Point point -> !point.isEmpty();
664+
case LineString lineString -> !lineString.isEmpty();
665+
case Polygon polygon -> !polygon.isEmpty();
666+
case MultiLineString multiLineString -> !multiLineString.isEmpty();
667+
case MultiPolygon multiPolygon -> !multiPolygon.isEmpty();
668+
case MultiPoint multiPoint -> !multiPoint.isEmpty();
669+
case GeometryCollection geometryCollection ->
670+
IntStream.range(0, geometryCollection.getNumGeometries())
671+
.mapToObj(geometryCollection::getGeometryN)
672+
.anyMatch(GeometryEncoder::hasGeometry);
673+
case null, default -> false;
674+
};
675+
}
637676
}
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
package org.maplibre.mlt.converter.encodings;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertFalse;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import java.net.URI;
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.params.ParameterizedTest;
12+
import org.junit.jupiter.params.provider.EnumSource;
13+
import org.junit.jupiter.params.provider.ValueSource;
14+
import org.locationtech.jts.geom.Coordinate;
15+
import org.locationtech.jts.geom.Geometry;
16+
import org.locationtech.jts.geom.GeometryFactory;
17+
import org.locationtech.jts.geom.LineString;
18+
import org.locationtech.jts.geom.Point;
19+
import org.locationtech.jts.geom.Polygon;
20+
import org.maplibre.mlt.converter.geometry.GeometryType;
21+
import org.maplibre.mlt.converter.geometry.Vertex;
22+
import org.maplibre.mlt.data.Feature;
23+
24+
class GeometryEncoderTest {
25+
@Test
26+
void nullGeom() {
27+
assertHasGeometry(null, false);
28+
}
29+
30+
@Test
31+
void nullFeature() {
32+
assertFalse(GeometryEncoder.hasGeometry((Feature) null));
33+
}
34+
35+
@ParameterizedTest
36+
@EnumSource(GeometryType.class)
37+
void validGeometries_return_true(final GeometryType type) {
38+
assertTrue(GeometryEncoder.hasGeometry(makeGeometry(type)), type.name());
39+
}
40+
41+
@ParameterizedTest
42+
@EnumSource(GeometryType.class)
43+
void emptyGeometries_return_false(final GeometryType type) {
44+
assertFalse(GeometryEncoder.hasGeometry(makeEmptyGeometry(type)), type.name());
45+
}
46+
47+
@Test
48+
void emptyPoint() {
49+
assertFalse(GeometryEncoder.hasGeometry(GF.createPoint((Coordinate) null)));
50+
}
51+
52+
@Test
53+
void pointCollection() {
54+
final var collection =
55+
GF.createGeometryCollection(new Geometry[] {GF.createPoint(coord(0, 0))});
56+
assertHasGeometry(collection, true);
57+
}
58+
59+
@Test
60+
void lineStringCollection() {
61+
final var collection =
62+
GF.createGeometryCollection(
63+
new Geometry[] {GF.createLineString(new Coordinate[] {coord(0, 0), coord(1, 1)})});
64+
assertHasGeometry(collection, true);
65+
}
66+
67+
@Test
68+
void polygonCollection() {
69+
final var collection = GF.createGeometryCollection(new Geometry[] {makePolygon(SAMPLE_SQUARE)});
70+
assertHasGeometry(collection, true);
71+
}
72+
73+
@Test
74+
void collectionEmpty() {
75+
assertHasGeometry(GF.createGeometryCollection(new Geometry[] {}), false);
76+
}
77+
78+
@Test
79+
void emptyGeomCollection() {
80+
final var collection = GF.createGeometryCollection(new Geometry[] {GF.createEmpty(2)});
81+
assertHasGeometry(collection, false);
82+
}
83+
84+
@Test
85+
void collectionAnyNotEmpty() {
86+
final var collection =
87+
GF.createGeometryCollection(
88+
new Geometry[] {GF.createEmpty(2), GF.createPoint(coord(0, 0))});
89+
assertHasGeometry(collection, true);
90+
}
91+
92+
@Test
93+
void collectionAllEmpty() {
94+
final var collection =
95+
GF.createGeometryCollection(
96+
new Geometry[] {GF.createEmpty(2), GF.createLineString(new Coordinate[] {})});
97+
assertHasGeometry(collection, false);
98+
}
99+
100+
@ParameterizedTest
101+
@ValueSource(booleans = {true, false})
102+
void nestedCollection(final boolean hasContent) {
103+
final var inner =
104+
hasContent
105+
? GF.createGeometryCollection(new Geometry[] {GF.createPoint(coord(0, 0))})
106+
: GF.createGeometryCollection(new Geometry[] {GF.createEmpty(2)});
107+
final var outer = GF.createGeometryCollection(new Geometry[] {inner});
108+
assertHasGeometry(outer, hasContent);
109+
}
110+
111+
@Test
112+
void collectionLineString() throws Exception {
113+
final var point = GF.createPoint(coord(10, 20));
114+
final var lineString = GF.createLineString(new Coordinate[] {coord(30, 40), coord(50, 60)});
115+
final var geometryCollection = GF.createGeometryCollection(new Geometry[] {point, lineString});
116+
117+
assertFlattenedEquals(
118+
runPrepareGeometry(List.of(geometryCollection)),
119+
runPrepareGeometry(List.of(point, lineString)));
120+
}
121+
122+
@Test
123+
void collectionNested() throws Exception {
124+
final var point = GF.createPoint(coord(5, 10));
125+
final var innerCollection = GF.createGeometryCollection(new Geometry[] {point});
126+
final var outerCollection = GF.createGeometryCollection(new Geometry[] {innerCollection});
127+
128+
assertEquals(
129+
runPrepareGeometry(List.of(outerCollection)).geometryTypes,
130+
runPrepareGeometry(List.of(point)).geometryTypes);
131+
assertEquals(
132+
runPrepareGeometry(List.of(outerCollection)).numGeometries,
133+
runPrepareGeometry(List.of(point)).numGeometries);
134+
assertEquals(
135+
runPrepareGeometry(List.of(outerCollection)).vertexBuffer,
136+
runPrepareGeometry(List.of(point)).vertexBuffer);
137+
}
138+
139+
@Test
140+
void collectionMultiPolygon() throws Exception {
141+
final var multiPolygon =
142+
GF.createMultiPolygon(
143+
new Polygon[] {makePolygon(SAMPLE_SQUARE), makePolygon(SAMPLE_SQUARE_2)});
144+
final var geometryCollection = GF.createGeometryCollection(new Geometry[] {multiPolygon});
145+
146+
assertFlattenedEquals(
147+
runPrepareGeometry(List.of(geometryCollection)), runPrepareGeometry(List.of(multiPolygon)));
148+
}
149+
150+
@Test
151+
void collectionPolygon() throws Exception {
152+
final var geometryCollection =
153+
GF.createGeometryCollection(
154+
new Geometry[] {makePolygon(SAMPLE_SQUARE), makePolygon(SAMPLE_SQUARE_2)});
155+
156+
assertFlattenedEquals(
157+
runPrepareGeometry(List.of(geometryCollection)),
158+
runPrepareGeometry(List.of(makePolygon(SAMPLE_SQUARE), makePolygon(SAMPLE_SQUARE_2))));
159+
}
160+
161+
private Geometry makeGeometry(final GeometryType type) {
162+
return switch (type) {
163+
case POINT -> GF.createPoint(coord(0, 0));
164+
case LINESTRING -> GF.createLineString(new Coordinate[] {coord(0, 0), coord(1, 1)});
165+
case POLYGON -> makePolygon(SAMPLE_SQUARE);
166+
case MULTIPOINT -> {
167+
final var p1 = GF.createPoint(coord(0, 0));
168+
final var p2 = GF.createPoint(coord(1, 1));
169+
yield GF.createMultiPoint(new Point[] {p1, p2});
170+
}
171+
case MULTILINESTRING -> {
172+
final var l1 = GF.createLineString(new Coordinate[] {coord(0, 0), coord(1, 1)});
173+
final var l2 = GF.createLineString(new Coordinate[] {coord(2, 2), coord(3, 3)});
174+
yield GF.createMultiLineString(new LineString[] {l1, l2});
175+
}
176+
case MULTIPOLYGON -> GF.createMultiPolygon(new Polygon[] {makePolygon(SAMPLE_SQUARE)});
177+
};
178+
}
179+
180+
private Geometry makeEmptyGeometry(final GeometryType type) {
181+
return switch (type) {
182+
case POINT -> GF.createPoint((Coordinate) null);
183+
case LINESTRING -> GF.createLineString(new Coordinate[] {});
184+
case POLYGON -> GF.createPolygon(new Coordinate[] {});
185+
case MULTIPOINT -> GF.createMultiPoint(new Point[] {});
186+
case MULTILINESTRING -> GF.createMultiLineString(new LineString[] {});
187+
case MULTIPOLYGON -> GF.createMultiPolygon(new Polygon[] {});
188+
};
189+
}
190+
191+
private static final GeometryFactory GF = new GeometryFactory();
192+
private static final Coordinate[] SAMPLE_SQUARE = {
193+
coord(0, 0), coord(1, 0), coord(1, 1), coord(0, 1), coord(0, 0)
194+
};
195+
private static final Coordinate[] SAMPLE_SQUARE_2 = {
196+
coord(2, 2), coord(3, 2), coord(3, 3), coord(2, 3), coord(2, 2)
197+
};
198+
199+
private static Coordinate coord(final double x, final double y) {
200+
return new Coordinate(x, y);
201+
}
202+
203+
private static Polygon makePolygon(final Coordinate[] coords) {
204+
return GF.createPolygon(coords);
205+
}
206+
207+
private static void assertHasGeometry(final Geometry geom, final boolean expected) {
208+
assertEquals(expected, GeometryEncoder.hasGeometry(geom));
209+
}
210+
211+
private static void assertFlattenedEquals(final PrepareResult a, final PrepareResult b) {
212+
assertEquals(a.geometryTypes, b.geometryTypes);
213+
assertEquals(a.numGeometries, b.numGeometries);
214+
assertEquals(a.vertexBuffer, b.vertexBuffer);
215+
assertEquals(a.numParts, b.numParts);
216+
assertEquals(a.numRings, b.numRings);
217+
}
218+
219+
private record PrepareResult(
220+
List<Integer> geometryTypes,
221+
List<Integer> numGeometries,
222+
List<Vertex> vertexBuffer,
223+
List<Integer> numParts,
224+
List<Integer> numRings) {}
225+
226+
private PrepareResult runPrepareGeometry(final List<Geometry> geometries) throws Exception {
227+
final var method =
228+
GeometryEncoder.class.getDeclaredMethod(
229+
"prepareGeometry",
230+
List.class,
231+
ArrayList.class,
232+
ArrayList.class,
233+
ArrayList.class,
234+
ArrayList.class,
235+
ArrayList.class,
236+
ArrayList.class,
237+
ArrayList.class,
238+
URI.class);
239+
method.setAccessible(true);
240+
241+
final var numGeometries = new ArrayList<Integer>();
242+
final var geometryTypes = new ArrayList<Integer>();
243+
final var vertexBuffer = new ArrayList<Vertex>();
244+
final var numParts = new ArrayList<Integer>();
245+
final var numRings = new ArrayList<Integer>();
246+
final var numTriangles = new ArrayList<Integer>();
247+
final var indexBuffer = new ArrayList<Integer>();
248+
249+
method.invoke(
250+
null,
251+
geometries,
252+
numGeometries,
253+
geometryTypes,
254+
vertexBuffer,
255+
numParts,
256+
numRings,
257+
numTriangles,
258+
indexBuffer,
259+
(URI) null);
260+
261+
return new PrepareResult(geometryTypes, numGeometries, vertexBuffer, numParts, numRings);
262+
}
263+
}

0 commit comments

Comments
 (0)