diff --git a/java/build.gradle b/java/build.gradle index 20e970c1..1d9c671c 100644 --- a/java/build.gradle +++ b/java/build.gradle @@ -27,11 +27,13 @@ dependencies { implementation 'org.apache.orc:orc-core:1.8.1' implementation 'com.github.davidmoten:hilbert-curve:0.2.3' implementation 'com.carrotsearch:hppc:0.10.0' + implementation "io.github.earcut4j:earcut4j:2.2.2" testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.3' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.3' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.3' testImplementation 'org.openjdk.jmh:jmh-core:1.37 ' testImplementation 'org.openjdk.jmh:jmh-generator-annprocess:1.37' + testImplementation "org.mockito:mockito-core:3.+" } test { diff --git a/java/src/jmh/java/com/mlt/BenchmarkUtils.java b/java/src/jmh/java/com/mlt/BenchmarkUtils.java index 07917840..82ab35c2 100644 --- a/java/src/jmh/java/com/mlt/BenchmarkUtils.java +++ b/java/src/jmh/java/com/mlt/BenchmarkUtils.java @@ -49,7 +49,10 @@ public static void encodeTile( .collect(Collectors.toMap(l -> l, l -> optimization)); var encodedMltTile = MltConverter.convertMvt( - encodedMvtTile.getRight(), new ConversionConfig(true, true, optimizations), metadata); + encodedMvtTile.getRight(), + new ConversionConfig(true, true, optimizations), + metadata, + false); encodedMltTiles.put(z, encodedMltTile); } diff --git a/java/src/main/java/com/mlt/cli/Encode.java b/java/src/main/java/com/mlt/cli/Encode.java index 416876ea..c74e26b6 100644 --- a/java/src/main/java/com/mlt/cli/Encode.java +++ b/java/src/main/java/com/mlt/cli/Encode.java @@ -218,7 +218,7 @@ public static void main(String[] args) { var conversionConfig = new ConversionConfig(includeIds, useAdvancedEncodingSchemes, optimizations); Timer timer = new Timer(); - var mlTile = MltConverter.convertMvt(decodedMvTile, conversionConfig, tileMetadata); + var mlTile = MltConverter.convertMvt(decodedMvTile, conversionConfig, tileMetadata, false); if (willTime) timer.stop("encoding"); if (willOutput) { Path outputPath = null; diff --git a/java/src/main/java/com/mlt/converter/MltConverter.java b/java/src/main/java/com/mlt/converter/MltConverter.java index 754584c1..d3092ef9 100644 --- a/java/src/main/java/com/mlt/converter/MltConverter.java +++ b/java/src/main/java/com/mlt/converter/MltConverter.java @@ -169,7 +169,8 @@ public static MltTilesetMetadata.TileSetMetadata createTilesetMetadata( public static byte[] convertMvt( MapboxVectorTile mvt, ConversionConfig config, - MltTilesetMetadata.TileSetMetadata tilesetMetadata) + MltTilesetMetadata.TileSetMetadata tilesetMetadata, + boolean triangulatePolygons) throws IOException { var physicalLevelTechnique = config.useAdvancedEncodingSchemes() @@ -192,7 +193,12 @@ public static byte[] convertMvt( var result = sortFeaturesAndEncodeGeometryColumn( - config, featureTableOptimizations, mvtFeatures, mvtFeatures, physicalLevelTechnique); + config, + featureTableOptimizations, + mvtFeatures, + mvtFeatures, + physicalLevelTechnique, + triangulatePolygons); var sortedFeatures = result.getLeft(); var encodedGeometryColumn = result.getRight(); var encodedGeometryFieldMetadata = @@ -266,7 +272,8 @@ private static byte[] encodePropertyColumns( FeatureTableOptimizations featureTableOptimizations, List sortedFeatures, List mvtFeatures, - PhysicalLevelTechnique physicalLevelTechnique) { + PhysicalLevelTechnique physicalLevelTechnique, + Boolean triangulatePolygons) { /* * Following simple strategy is currently used for ordering the features when sorting is enabled: * - if id column is present and ids should not be reassigned -> sort id column @@ -294,7 +301,8 @@ private static byte[] encodePropertyColumns( new GeometryEncoder.SortSettings( isColumnSortable && featureTableOptimizations.allowIdRegeneration(), ids); var encodedGeometryColumn = - GeometryEncoder.encodeGeometryColumn(geometries, physicalLevelTechnique, sortSettings); + GeometryEncoder.encodeGeometryColumn( + geometries, physicalLevelTechnique, sortSettings, triangulatePolygons); if (encodedGeometryColumn.geometryColumnSorted()) { sortedFeatures = diff --git a/java/src/main/java/com/mlt/converter/encodings/GeometryEncoder.java b/java/src/main/java/com/mlt/converter/encodings/GeometryEncoder.java index 9b391db3..41d9b32f 100644 --- a/java/src/main/java/com/mlt/converter/encodings/GeometryEncoder.java +++ b/java/src/main/java/com/mlt/converter/encodings/GeometryEncoder.java @@ -5,6 +5,7 @@ import com.mlt.converter.CollectionUtils; import com.mlt.converter.geometry.*; +import com.mlt.converter.triangulation.VectorTileConverter; import com.mlt.metadata.stream.*; import java.util.*; import java.util.function.Function; @@ -33,12 +34,15 @@ private GeometryEncoder() {} public static EncodedGeometryColumn encodeGeometryColumn( List geometries, PhysicalLevelTechnique physicalLevelTechnique, - SortSettings sortSettings) { + SortSettings sortSettings, + boolean triangulatePolygons) { var geometryTypes = new ArrayList(); var numGeometries = new ArrayList(); var numParts = new ArrayList(); var numRings = new ArrayList(); var vertexBuffer = new ArrayList(); + var numTrianglesPerPolygon = new ArrayList(); + var indexBuffer = new ArrayList(); var containsPolygon = geometries.stream() .anyMatch( @@ -74,6 +78,11 @@ public static EncodedGeometryColumn encodeGeometryColumn( var polygon = (Polygon) geometry; var vertices = flatPolygon(polygon, numParts, numRings); vertexBuffer.addAll(vertices); + if (triangulatePolygons) { + var triangulatedPolygon = new VectorTileConverter(polygon); + indexBuffer.addAll(triangulatedPolygon.getIndexBuffer()); + numTrianglesPerPolygon.addAll(triangulatedPolygon.getNumTrianglesPerPolygon()); + } break; } case Geometry.TYPENAME_MULTILINESTRING: @@ -102,6 +111,11 @@ public static EncodedGeometryColumn encodeGeometryColumn( var vertices = flatPolygon(polygon, numParts, numRings); vertexBuffer.addAll(vertices); } + if (triangulatePolygons) { + var triangulatedPolygon = new VectorTileConverter(multiPolygon); + indexBuffer.addAll(triangulatedPolygon.getIndexBuffer()); + numTrianglesPerPolygon.addAll(triangulatedPolygon.getNumTrianglesPerPolygon()); + } break; } case Geometry.TYPENAME_MULTIPOINT: @@ -161,6 +175,21 @@ public static EncodedGeometryColumn encodeGeometryColumn( Arrays.stream(zigZagDeltaVertexBuffer).boxed().collect(Collectors.toList()), physicalLevelTechnique, false); + + var encodedIndexBuffer = + IntegerEncoder.encodeIntStream( + indexBuffer, + physicalLevelTechnique, + false, + PhysicalStreamType.OFFSET, + new LogicalStreamType(OffsetType.INDEX)); + var encodedNumTrianglesBuffer = + IntegerEncoder.encodeIntStream( + numTrianglesPerPolygon, + physicalLevelTechnique, + false, + PhysicalStreamType.OFFSET, + new LogicalStreamType(OffsetType.INDEX)); // TODO: should we do a potential recursive encoding again var encodedVertexDictionary = IntegerEncoder.encodeInt( @@ -251,11 +280,16 @@ public static EncodedGeometryColumn encodeGeometryColumn( Arrays.stream(zigZagDeltaVertexBuffer).boxed().collect(Collectors.toList()), physicalLevelTechnique); - return new EncodedGeometryColumn( - numStreams + 1, - ArrayUtils.addAll(encodedTopologyStreams, encodedVertexBufferStream), - maxVertexValue, - geometryColumnSorted); + var encodedGeometryColumn = + new EncodedGeometryColumn( + numStreams + 1, + ArrayUtils.addAll(encodedTopologyStreams, encodedVertexBufferStream), + maxVertexValue, + geometryColumnSorted); + return triangulatePolygons + ? buildTriangulatedEncodedGeometryColumn( + encodedGeometryColumn, encodedIndexBuffer, encodedNumTrianglesBuffer) + : encodedGeometryColumn; } else if (dictionaryEncodedSize < plainVertexBufferSize && dictionaryEncodedSize <= mortonDictionaryEncodedSize) { var encodedVertexOffsetStream = @@ -269,13 +303,18 @@ public static EncodedGeometryColumn encodeGeometryColumn( encodeVertexBuffer( Arrays.stream(zigZagDeltaVertexDictionary).boxed().collect(Collectors.toList()), physicalLevelTechnique); - - return new EncodedGeometryColumn( - numStreams + 2, - CollectionUtils.concatByteArrays( - encodedTopologyStreams, encodedVertexOffsetStream, encodedVertexDictionaryStream), - maxVertexValue, - false); + var encodedGeometryColumn = + new EncodedGeometryColumn( + numStreams + 2, + CollectionUtils.concatByteArrays( + encodedTopologyStreams, encodedVertexOffsetStream, encodedVertexDictionaryStream), + maxVertexValue, + false); + + return triangulatePolygons + ? buildTriangulatedEncodedGeometryColumn( + encodedGeometryColumn, encodedIndexBuffer, encodedNumTrianglesBuffer) + : encodedGeometryColumn; } else { var encodedMortonVertexOffsetStream = IntegerEncoder.encodeIntStream( @@ -292,14 +331,20 @@ public static EncodedGeometryColumn encodeGeometryColumn( zOrderCurve.coordinateShift(), physicalLevelTechnique); - return new EncodedGeometryColumn( - numStreams + 2, - CollectionUtils.concatByteArrays( - encodedTopologyStreams, - encodedMortonVertexOffsetStream, - encodedMortonEncodedVertexDictionaryStream), - maxVertexValue, - geometryColumnSorted); + var encodedGeometryColumn = + new EncodedGeometryColumn( + numStreams + 2, + CollectionUtils.concatByteArrays( + encodedTopologyStreams, + encodedMortonVertexOffsetStream, + encodedMortonEncodedVertexDictionaryStream), + maxVertexValue, + geometryColumnSorted); + + return triangulatePolygons + ? buildTriangulatedEncodedGeometryColumn( + encodedGeometryColumn, encodedIndexBuffer, encodedNumTrianglesBuffer) + : encodedGeometryColumn; } } @@ -426,4 +471,16 @@ private static byte[] encodeVertexBuffer( return ArrayUtils.addAll(encodedMetadata, encodedValues); } + + private static EncodedGeometryColumn buildTriangulatedEncodedGeometryColumn( + EncodedGeometryColumn encodedGeometryColumn, + byte[] encodedIndexBuffer, + byte[] encodedNumTrianglesPerPolygon) { + return new EncodedGeometryColumn( + encodedGeometryColumn.numStreams + 2, + CollectionUtils.concatByteArrays( + encodedGeometryColumn.encodedValues, encodedIndexBuffer, encodedNumTrianglesPerPolygon), + encodedGeometryColumn.maxVertexValue, + encodedGeometryColumn.geometryColumnSorted); + } } diff --git a/java/src/main/java/com/mlt/converter/triangulation/TriangulatedPolygon.java b/java/src/main/java/com/mlt/converter/triangulation/TriangulatedPolygon.java new file mode 100644 index 00000000..af63a7d5 --- /dev/null +++ b/java/src/main/java/com/mlt/converter/triangulation/TriangulatedPolygon.java @@ -0,0 +1,23 @@ +package com.mlt.converter.triangulation; + +import java.util.ArrayList; +import java.util.List; + +public class TriangulatedPolygon { + private final int numTrianglesPerPolygon; + + private final ArrayList indexBuffer; + + TriangulatedPolygon(ArrayList indexBuffer, int numTriangles) { + this.numTrianglesPerPolygon = numTriangles; + this.indexBuffer = indexBuffer; + } + + public Integer getNumTrianglesPerPolygon() { + return numTrianglesPerPolygon; + } + + public List getIndexBuffer() { + return indexBuffer; + } +} diff --git a/java/src/main/java/com/mlt/converter/triangulation/TriangulationUtils.java b/java/src/main/java/com/mlt/converter/triangulation/TriangulationUtils.java new file mode 100644 index 00000000..47cb7054 --- /dev/null +++ b/java/src/main/java/com/mlt/converter/triangulation/TriangulationUtils.java @@ -0,0 +1,76 @@ +package com.mlt.converter.triangulation; + +import earcut4j.Earcut; +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.lang3.ArrayUtils; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Polygon; + +public class TriangulationUtils { + private TriangulationUtils() {} + + public static TriangulatedPolygon triangulatePolygon(Polygon polygon) { + var convertedCoordinates = convertCoordinates(polygon.getCoordinates()); + + List triangles = Earcut.earcut(convertedCoordinates, null, 2); + + ArrayList indexBuffer = new ArrayList<>(triangles); + var numTriangles = triangles.size() / 3; + + return new TriangulatedPolygon(indexBuffer, numTriangles); + } + + public static TriangulatedPolygon triangulatePolygonWithHoles(MultiPolygon multiPolygon) { + var holeIndex = 0; + + ArrayList multiPolygonCoordinates = new ArrayList<>(); + ArrayList holeIndices = new ArrayList<>(); + + for (int i = 0; i < multiPolygon.getNumGeometries(); i++) { + // assertion: first polygon defines the outer linear ring and the other polygons define its + // holes! + if (i == 0) { + holeIndex = multiPolygon.getGeometryN(i).getCoordinates().length; + holeIndices.add(holeIndex); + } else if (i != multiPolygon.getNumGeometries() - 1) { + holeIndex += multiPolygon.getGeometryN(i).getCoordinates().length; + holeIndices.add(holeIndex); + } + + var coordinates = multiPolygon.getGeometryN(i).getCoordinates(); + for (Coordinate coordinate : coordinates) { + multiPolygonCoordinates.add(coordinate.x); + multiPolygonCoordinates.add(coordinate.y); + if (!Double.isNaN(coordinate.z)) { + multiPolygonCoordinates.add(coordinate.z); + } + } + } + + var doubleArray = ArrayUtils.toPrimitive(multiPolygonCoordinates.toArray(new Double[0])); + List triangleVertices = + Earcut.earcut(doubleArray, holeIndices.stream().mapToInt(Integer::intValue).toArray(), 2); + + ArrayList indexBuffer = new ArrayList<>(triangleVertices); + var numTriangles = triangleVertices.size() / 3; + + return new TriangulatedPolygon(indexBuffer, numTriangles); + } + + private static double[] convertCoordinates(Coordinate[] coordinates) { + ArrayList convertedCoordinates = new ArrayList<>(); + + for (Coordinate coordinate : coordinates) { + convertedCoordinates.add(coordinate.x); + convertedCoordinates.add(coordinate.y); + if (!Double.isNaN(coordinate.z)) { + convertedCoordinates.add(coordinate.z); + } + } + Double[] array = convertedCoordinates.toArray(new Double[0]); + + return ArrayUtils.toPrimitive(array); + } +} diff --git a/java/src/main/java/com/mlt/converter/triangulation/VectorTileConverter.java b/java/src/main/java/com/mlt/converter/triangulation/VectorTileConverter.java new file mode 100644 index 00000000..f0bed879 --- /dev/null +++ b/java/src/main/java/com/mlt/converter/triangulation/VectorTileConverter.java @@ -0,0 +1,117 @@ +package com.mlt.converter.triangulation; + +import com.mlt.converter.encodings.EncodingUtils; +import com.mlt.converter.encodings.IntegerEncoder; +import com.mlt.converter.mvt.MapboxVectorTile; +import com.mlt.converter.mvt.MvtUtils; +import com.mlt.data.Feature; +import com.mlt.metadata.stream.LogicalStreamType; +import com.mlt.metadata.stream.OffsetType; +import com.mlt.metadata.stream.PhysicalLevelTechnique; +import com.mlt.metadata.stream.PhysicalStreamType; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import no.ecc.vectortile.VectorTileDecoder; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Polygon; + +public class VectorTileConverter { + private final ArrayList numTrianglesPerPolygon = new ArrayList<>(); + + private final ArrayList indexBuffer = new ArrayList<>(); + + public VectorTileConverter(Path vectorTilePath) throws IOException { + var vectorTile = MvtUtils.decodeMvt(vectorTilePath); + this.triangulatePolygons(vectorTile); + } + + public VectorTileConverter(byte[] mvtTile) throws IOException { + var vectorTile = MvtUtils.decodeMvtFast(mvtTile); + this.triangulatePolygons(vectorTile); + } + + public VectorTileConverter(Polygon polygon) { + this.triangulatePolygon(polygon); + } + + public VectorTileConverter(MultiPolygon multiPolygon) { + this.triangulateMultiPolygon(multiPolygon); + } + + public List getNumTrianglesPerPolygon() { + return numTrianglesPerPolygon; + } + + public List getIndexBuffer() { + return indexBuffer; + } + + public byte[] getEncodedIndexBuffer() { + return IntegerEncoder.encodeIntStream( + this.indexBuffer, + PhysicalLevelTechnique.FAST_PFOR, + false, + PhysicalStreamType.DATA, + new LogicalStreamType(OffsetType.INDEX)); + } + + public byte[] getEncodedNumberOfTrianglesPerPolygon() { + return IntegerEncoder.encodeIntStream( + this.numTrianglesPerPolygon, + PhysicalLevelTechnique.FAST_PFOR, + false, + PhysicalStreamType.DATA, + new LogicalStreamType(OffsetType.INDEX)); + } + + public byte[] getGzippedIndexBuffer() throws IOException { + return EncodingUtils.gzip(this.getEncodedIndexBuffer()); + } + + public byte[] getGzippedNumberOfTrianglesPerPolygon() throws IOException { + return EncodingUtils.gzip(this.getEncodedNumberOfTrianglesPerPolygon()); + } + + private void triangulatePolygons(MapboxVectorTile vectorTile) { + vectorTile.layers().forEach(layer -> layer.features().forEach(this::triangulatePolygonFeature)); + } + + private void triangulatePolygons(List decodedTile) { + for (VectorTileDecoder.Feature feature : decodedTile) { + var geometry = feature.getGeometry().toString(); + if (geometry.contains(Geometry.TYPENAME_MULTIPOLYGON.toUpperCase())) { + triangulateMultiPolygon((MultiPolygon) feature.getGeometry()); + } else if (geometry.contains(Geometry.TYPENAME_POLYGON.toUpperCase())) { + triangulatePolygon((Polygon) feature.getGeometry()); + } + } + } + + private void triangulatePolygonFeature(Feature feature) { + if (feature.geometry().getGeometryType().equals(Geometry.TYPENAME_POLYGON)) { + var triangulatedPolygon = TriangulationUtils.triangulatePolygon((Polygon) feature.geometry()); + this.indexBuffer.addAll(triangulatedPolygon.getIndexBuffer()); + this.numTrianglesPerPolygon.add(triangulatedPolygon.getNumTrianglesPerPolygon()); + } else if (feature.geometry().getGeometryType().equals(Geometry.TYPENAME_MULTIPOLYGON)) { + var triangulatedPolygon = + TriangulationUtils.triangulatePolygonWithHoles((MultiPolygon) feature.geometry()); + this.indexBuffer.addAll(triangulatedPolygon.getIndexBuffer()); + this.numTrianglesPerPolygon.add(triangulatedPolygon.getNumTrianglesPerPolygon()); + } + } + + private void triangulatePolygon(Polygon polygon) { + var triangulatedPolygon = TriangulationUtils.triangulatePolygon(polygon); + this.indexBuffer.addAll(triangulatedPolygon.getIndexBuffer()); + this.numTrianglesPerPolygon.add(triangulatedPolygon.getNumTrianglesPerPolygon()); + } + + private void triangulateMultiPolygon(MultiPolygon multiPolygon) { + var triangulatedPolygon = TriangulationUtils.triangulatePolygonWithHoles(multiPolygon); + this.indexBuffer.addAll(triangulatedPolygon.getIndexBuffer()); + this.numTrianglesPerPolygon.add(triangulatedPolygon.getNumTrianglesPerPolygon()); + } +} diff --git a/java/src/main/java/com/mlt/decoder/vectorized/VectorizedGeometryDecoder.java b/java/src/main/java/com/mlt/decoder/vectorized/VectorizedGeometryDecoder.java index 735b4531..ca7dd5d1 100644 --- a/java/src/main/java/com/mlt/decoder/vectorized/VectorizedGeometryDecoder.java +++ b/java/src/main/java/com/mlt/decoder/vectorized/VectorizedGeometryDecoder.java @@ -1,8 +1,6 @@ package com.mlt.decoder.vectorized; -import com.mlt.metadata.stream.DictionaryType; -import com.mlt.metadata.stream.MortonEncodedStreamMetadata; -import com.mlt.metadata.stream.StreamMetadataDecoder; +import com.mlt.metadata.stream.*; import com.mlt.vector.VectorType; import com.mlt.vector.geometry.GeometryVector; import com.mlt.vector.geometry.TopologyVector; @@ -19,6 +17,8 @@ public record GeometryColumn( IntBuffer numRings, IntBuffer vertexOffsets, IntBuffer vertexBuffer, + Optional indexBuffer, + Optional numTrianglesPerPolygonBuffer, Optional mortonSettings) {} private VectorizedGeometryDecoder() {} @@ -35,6 +35,8 @@ public static VectorizedGeometryDecoder.GeometryColumn decodeGeometryColumn( IntBuffer numRings = null; IntBuffer vertexOffsets = null; IntBuffer vertexBuffer = null; + Optional indexBuffer = Optional.empty(); + Optional numTrianglesPerPolygonBuffer = Optional.empty(); Optional mortonSettings = Optional.empty(); for (var i = 0; i < numStreams - 1; i++) { var geometryStreamMetadata = StreamMetadataDecoder.decode(tile, offset); @@ -61,8 +63,25 @@ public static VectorizedGeometryDecoder.GeometryColumn decodeGeometryColumn( } break; case OFFSET: - vertexOffsets = - VectorizedIntegerDecoder.decodeIntStream(tile, offset, geometryStreamMetadata, false); + switch (geometryStreamMetadata.logicalStreamType().offsetType()) { + case INDEX: + if (indexBuffer.isEmpty()) { + indexBuffer = + Optional.of( + VectorizedIntegerDecoder.decodeIntStream( + tile, offset, geometryStreamMetadata, false)); + } else { + numTrianglesPerPolygonBuffer = + Optional.of( + VectorizedIntegerDecoder.decodeIntStream( + tile, offset, geometryStreamMetadata, false)); + } + break; + default: + vertexOffsets = + VectorizedIntegerDecoder.decodeIntStream( + tile, offset, geometryStreamMetadata, false); + } break; case DATA: if (DictionaryType.VERTEX.equals( @@ -91,6 +110,8 @@ public static VectorizedGeometryDecoder.GeometryColumn decodeGeometryColumn( numRings, vertexOffsets, vertexBuffer, + indexBuffer, + numTrianglesPerPolygonBuffer, mortonSettings); } diff --git a/java/src/test/java/com/mlt/benchmarks/CompressionBenchmarks.java b/java/src/test/java/com/mlt/benchmarks/CompressionBenchmarks.java index 3f24f722..86973c80 100644 --- a/java/src/test/java/com/mlt/benchmarks/CompressionBenchmarks.java +++ b/java/src/test/java/com/mlt/benchmarks/CompressionBenchmarks.java @@ -143,7 +143,7 @@ private static Pair getBenchmarksAndVerifyTiles( var mlTile = MltConverter.convertMvt( - mvTile, new ConversionConfig(true, true, optimizations), tileMetadata); + mvTile, new ConversionConfig(true, true, optimizations), tileMetadata, false); if (reassignableLayers.isEmpty()) { /* Only test when the ids are not reassigned since it is verified based on the other tests */ diff --git a/java/src/test/java/com/mlt/converter/encodings/GeometryEncodingTest.java b/java/src/test/java/com/mlt/converter/encodings/GeometryEncodingTest.java new file mode 100644 index 00000000..2b01a023 --- /dev/null +++ b/java/src/test/java/com/mlt/converter/encodings/GeometryEncodingTest.java @@ -0,0 +1,134 @@ +package com.mlt.converter.encodings; + +import com.mlt.TestSettings; +import com.mlt.converter.mvt.MvtUtils; +import com.mlt.converter.triangulation.TriangulationUtils; +import com.mlt.decoder.vectorized.VectorizedGeometryDecoder; +import com.mlt.metadata.stream.PhysicalLevelTechnique; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import me.lemire.integercompression.IntWrapper; +import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.util.Assert; + +public class GeometryEncodingTest { + Path mvtFilePath = Paths.get(TestSettings.BING_MVT_PATH, "4-8-5" + ".mvt"); + + PhysicalLevelTechnique physicalLevelTechnique = PhysicalLevelTechnique.FAST_PFOR; + + @Test + public void testTriangulatedGeometryEncodingForTile() throws IOException { + var decodedMvTile = MvtUtils.decodeMvt(mvtFilePath); + + var geometries = new ArrayList(); + var featureIds = new ArrayList(); + + decodedMvTile + .layers() + .forEach( + layer -> { + layer + .features() + .forEach( + feature -> { + geometries.add(feature.geometry()); + featureIds.add(feature.id()); + }); + }); + + var sortSettings = new GeometryEncoder.SortSettings(false, featureIds); + var encodedGeometryColumn = + GeometryEncoder.encodeGeometryColumn( + geometries, physicalLevelTechnique, sortSettings, true); + var decodedGeometryColumn = + VectorizedGeometryDecoder.decodeGeometryColumn( + encodedGeometryColumn.encodedValues(), + encodedGeometryColumn.numStreams(), + new IntWrapper(0)); + + Assert.isTrue(decodedGeometryColumn.indexBuffer().isPresent()); + Assert.isTrue(decodedGeometryColumn.numTrianglesPerPolygonBuffer().isPresent()); + } + + @Test + public void testTriangulateGeometryColumnForPolygonLayer() throws IOException { + var decodedMvTile = MvtUtils.decodeMvt(mvtFilePath); + + var geometries = new ArrayList(); + var featureIds = new ArrayList(); + var polygonFeature = decodedMvTile.layers().get(5).features().get(0); + geometries.add(polygonFeature.geometry()); + featureIds.add(polygonFeature.id()); + + var sortSettings = new GeometryEncoder.SortSettings(false, featureIds); + var encodedGeometryColumn = + GeometryEncoder.encodeGeometryColumn( + geometries, physicalLevelTechnique, sortSettings, true); + var decodedGeometryColumn = + VectorizedGeometryDecoder.decodeGeometryColumn( + encodedGeometryColumn.encodedValues(), + encodedGeometryColumn.numStreams(), + new IntWrapper(0)); + + var expectedTriangulatedPolygon = + TriangulationUtils.triangulatePolygon((Polygon) polygonFeature.geometry()); + var indexBuffer = expectedTriangulatedPolygon.getIndexBuffer(); + + var expectedIndexBuffer = new int[indexBuffer.size()]; + for (int i = 0; i < indexBuffer.size(); i++) { + expectedIndexBuffer[i] = indexBuffer.get(i); + } + + Assert.isTrue(decodedGeometryColumn.indexBuffer().isPresent()); + Assert.isTrue(decodedGeometryColumn.numTrianglesPerPolygonBuffer().isPresent()); + Assert.equals( + decodedGeometryColumn.numTrianglesPerPolygonBuffer().get().array()[0], + expectedTriangulatedPolygon.getNumTrianglesPerPolygon()); + Assert.isTrue( + Arrays.equals(decodedGeometryColumn.indexBuffer().get().array(), expectedIndexBuffer)); + } + + @Test + public void testTriangulateGeometryColumnForMultiPolygonLayer() throws IOException { + var decodedMvTile = MvtUtils.decodeMvt(mvtFilePath); + + var geometries = new ArrayList(); + var featureIds = new ArrayList(); + var polygonFeature = decodedMvTile.layers().get(0).features().get(0); + geometries.add(polygonFeature.geometry()); + featureIds.add(polygonFeature.id()); + var sortSettings = new GeometryEncoder.SortSettings(false, featureIds); + + var encodedGeometryColumn = + GeometryEncoder.encodeGeometryColumn( + geometries, physicalLevelTechnique, sortSettings, true); + var decodedGeometryColumn = + VectorizedGeometryDecoder.decodeGeometryColumn( + encodedGeometryColumn.encodedValues(), + encodedGeometryColumn.numStreams(), + new IntWrapper(0)); + + var expectedTriangulatedMultiPolygon = + TriangulationUtils.triangulatePolygonWithHoles((MultiPolygon) polygonFeature.geometry()); + var indexBuffer = expectedTriangulatedMultiPolygon.getIndexBuffer(); + + var expectedIndexBuffer = new int[indexBuffer.size()]; + for (int i = 0; i < indexBuffer.size(); i++) { + expectedIndexBuffer[i] = indexBuffer.get(i); + } + + Assert.isTrue(decodedGeometryColumn.indexBuffer().isPresent()); + Assert.isTrue(decodedGeometryColumn.numTrianglesPerPolygonBuffer().isPresent()); + Assert.equals( + decodedGeometryColumn.numTrianglesPerPolygonBuffer().get().array()[0], + expectedTriangulatedMultiPolygon.getNumTrianglesPerPolygon()); + Assert.isTrue( + Arrays.equals(decodedGeometryColumn.indexBuffer().get().array(), expectedIndexBuffer)); + } +} diff --git a/java/src/test/java/com/mlt/decoder/MltDecoderBenchmark.java b/java/src/test/java/com/mlt/decoder/MltDecoderBenchmark.java index 6b5e78e8..67763750 100644 --- a/java/src/test/java/com/mlt/decoder/MltDecoderBenchmark.java +++ b/java/src/test/java/com/mlt/decoder/MltDecoderBenchmark.java @@ -193,7 +193,7 @@ private void benchmarkDecoding(String tileId) throws IOException { optimization); var mlTile = MltConverter.convertMvt( - mvTile, new ConversionConfig(true, true, optimizations), tileMetadata); + mvTile, new ConversionConfig(true, true, optimizations), tileMetadata, false); var mltTimeElapsed = 0L; for (int i = 0; i <= 200; i++) { diff --git a/java/src/test/java/com/mlt/decoder/MltDecoderTest.java b/java/src/test/java/com/mlt/decoder/MltDecoderTest.java index 6b4c0e31..8a686787 100644 --- a/java/src/test/java/com/mlt/decoder/MltDecoderTest.java +++ b/java/src/test/java/com/mlt/decoder/MltDecoderTest.java @@ -199,10 +199,10 @@ private DecodingResult testTile( var includeIds = true; var mlTile = MltConverter.convertMvt( - mvTile, new ConversionConfig(includeIds, false, optimizations), tileMetadata); + mvTile, new ConversionConfig(includeIds, false, optimizations), tileMetadata, false); var mlTileAdvanced = MltConverter.convertMvt( - mvTile, new ConversionConfig(includeIds, true, optimizations), tileMetadata); + mvTile, new ConversionConfig(includeIds, true, optimizations), tileMetadata, false); int numErrors = -1; int numErrorsAdvanced = -1; if (decoder == DecoderType.SEQUENTIAL || decoder == DecoderType.BOTH) { diff --git a/java/src/test/java/com/mlt/earcut/EarCutTriangulationTest.java b/java/src/test/java/com/mlt/earcut/EarCutTriangulationTest.java new file mode 100644 index 00000000..3166cfc1 --- /dev/null +++ b/java/src/test/java/com/mlt/earcut/EarCutTriangulationTest.java @@ -0,0 +1,158 @@ +package com.mlt.earcut; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.mlt.converter.triangulation.TriangulationUtils; +import com.mlt.converter.triangulation.VectorTileConverter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.*; +import org.locationtech.jts.util.Assert; + +class EarcutTriangulationTest { + @Test + void triangulateVectorTile() throws IOException { + var path = Paths.get("../test/fixtures/amazon/5_5_11.pbf").toAbsolutePath(); + + var polygonConverter = new VectorTileConverter(path); + + Assert.isTrue(polygonConverter.getNumTrianglesPerPolygon().size() > 0); + Assert.isTrue(polygonConverter.getIndexBuffer().size() > 0); + + logVectorizationInformation(polygonConverter); + } + + @Test + void triangulateVectorTile2() throws IOException { + var path = Paths.get("../test/fixtures/omt/13_4266_5468.mvt").toAbsolutePath(); + var encodedTile = Files.readAllBytes(path); + + var polygonConverter = new VectorTileConverter(encodedTile); + + Assert.isTrue(polygonConverter.getNumTrianglesPerPolygon().size() > 0); + Assert.isTrue(polygonConverter.getIndexBuffer().size() > 0); + logVectorizationInformation(polygonConverter); + } + + @Test + void verifyPolygonTriangulation() { + Polygon polygon = mock(Polygon.class); + + when(polygon.getCoordinates()).thenReturn(getQuadraticPolygonCoordinates()); + var triangulatedPolygon = TriangulationUtils.triangulatePolygon(polygon); + + Assert.isTrue(triangulatedPolygon.getNumTrianglesPerPolygon() == 2); + Assert.isTrue(triangulatedPolygon.getIndexBuffer().size() == 6); + } + + @Test + void verifyMultiPolygonTriangulation() { + MultiPolygon multiPolygon = mock(MultiPolygon.class); + Polygon outerPolygon = mock(Polygon.class); + when(outerPolygon.getCoordinates()).thenReturn(getQuadraticPolygonCoordinates()); + + Polygon innerPolygon = mock(Polygon.class); + Coordinate[] innerPolygonCoordinates = new Coordinate[4]; + innerPolygonCoordinates[0] = new Coordinate(0.25, 0.25); + innerPolygonCoordinates[1] = new Coordinate(0.75, 0.25); + innerPolygonCoordinates[2] = new Coordinate(0.75, 0.75); + innerPolygonCoordinates[3] = new Coordinate(0.25, 0.75); + + when(innerPolygon.getCoordinates()).thenReturn(innerPolygonCoordinates); + + when(multiPolygon.getNumGeometries()).thenReturn(2); + when(multiPolygon.getGeometryN(0)).thenReturn(outerPolygon); + when(multiPolygon.getGeometryN(1)).thenReturn(innerPolygon); + + var triangulatedPolygon = TriangulationUtils.triangulatePolygonWithHoles(multiPolygon); + + Assert.isTrue(triangulatedPolygon.getNumTrianglesPerPolygon() == 8); + Assert.isTrue(triangulatedPolygon.getIndexBuffer().size() == 24); + } + + private Coordinate[] getQuadraticPolygonCoordinates() { + Coordinate[] coordinates = new Coordinate[4]; + coordinates[0] = new Coordinate(0, 0); + coordinates[1] = new Coordinate(1, 0); + coordinates[2] = new Coordinate(1, 1); + coordinates[3] = new Coordinate(0, 1); + return coordinates; + } + + private int getByteSize(int[] array) { + return 4 * array.length; + } + + private void logVectorizationInformation(VectorTileConverter vectorTileConverter) + throws IOException { + var indexBufferSize = + getByteSize(vectorTileConverter.getIndexBuffer().stream().mapToInt(i -> i).toArray()); + var encodedIndexBuffer = vectorTileConverter.getEncodedIndexBuffer(); + var gzippedIndexBuffer = vectorTileConverter.getGzippedIndexBuffer(); + + var encodedPercentageOfOriginalSize = + (double) encodedIndexBuffer.length / (double) indexBufferSize * 100; + var gzippedPercentageOfOriginalSize = + (double) gzippedIndexBuffer.length / (double) indexBufferSize * 100; + + System.out.println("------------ IndexBuffer result ------------"); + System.out.println("#### Byte size of integer index array: " + indexBufferSize); + System.out.println("#### Byte size of encoded index array: " + encodedIndexBuffer.length); + System.out.println("#### Byte size of gzipped index array: " + gzippedIndexBuffer.length); + System.out.println("------"); + System.out.println( + "#### Array length of index integer array: " + vectorTileConverter.getIndexBuffer().size()); + System.out.println( + "#### Array length of encoded index byte array: " + encodedIndexBuffer.length); + System.out.println( + "#### Array length of gzipped index byte array: " + gzippedIndexBuffer.length); + System.out.println( + "---> Encoded vertices are " + + round(encodedPercentageOfOriginalSize, 2) + + " % the size of the original vertex buffer."); + System.out.println( + "---> Gzipped vertices are " + + round(gzippedPercentageOfOriginalSize, 2) + + " % the size of the original vertex buffer."); + System.out.println(); + + var numTrianglesSize = + getByteSize( + vectorTileConverter.getNumTrianglesPerPolygon().stream().mapToInt(i -> i).toArray()); + var encodedNumTriangles = vectorTileConverter.getEncodedNumberOfTrianglesPerPolygon(); + var gzippedNumTriangles = vectorTileConverter.getGzippedNumberOfTrianglesPerPolygon(); + var percentageOfOriginalNumTrianglesSize = + (double) encodedNumTriangles.length / (double) numTrianglesSize * 100; + var gzippedPercentageOfOriginalNumTrianglesSize = + (double) gzippedNumTriangles.length / (double) numTrianglesSize * 100; + + System.out.println("------------ NumTriangles result ------------"); + System.out.println("#### Byte size of integer index array: " + numTrianglesSize); + System.out.println("#### Byte size of encoded index array: " + encodedNumTriangles.length); + System.out.println("#### Byte size of gzipped index array: " + gzippedNumTriangles.length); + System.out.println("------"); + System.out.println( + "#### Array length of index integer array: " + + vectorTileConverter.getNumTrianglesPerPolygon().size()); + System.out.println( + "#### Array length of encoded index byte array: " + encodedNumTriangles.length); + System.out.println( + "#### Array length of gzipped index byte array: " + gzippedNumTriangles.length); + System.out.println( + "---> Encoded numTriangles are " + + round(percentageOfOriginalNumTrianglesSize, 2) + + " % the size of the original vertex buffer."); + System.out.println( + "---> Gzipped numTriangles are " + + round(gzippedPercentageOfOriginalNumTrianglesSize, 2) + + " % the size of the original vertex buffer."); + System.out.println(); + } + + public static double round(double value, int scale) { + return Math.round(value * Math.pow(10, scale)) / Math.pow(10, scale); + } +}