diff --git a/src/main/java/com/onthegomap/flatmap/LayerFeature.java b/src/main/java/com/onthegomap/flatmap/LayerFeature.java index 6d875700..d65b8a17 100644 --- a/src/main/java/com/onthegomap/flatmap/LayerFeature.java +++ b/src/main/java/com/onthegomap/flatmap/LayerFeature.java @@ -1,5 +1,6 @@ package com.onthegomap.flatmap; +import com.onthegomap.flatmap.VectorTileEncoder.VectorTileFeature; import java.util.Map; public record LayerFeature( @@ -10,6 +11,6 @@ public record LayerFeature( byte geomType, int[] commands, long id -) { +) implements VectorTileFeature { } diff --git a/src/main/java/com/onthegomap/flatmap/VectorTileEncoder.java b/src/main/java/com/onthegomap/flatmap/VectorTileEncoder.java index b5c5dff0..a8f902ce 100644 --- a/src/main/java/com/onthegomap/flatmap/VectorTileEncoder.java +++ b/src/main/java/com/onthegomap/flatmap/VectorTileEncoder.java @@ -40,8 +40,11 @@ import org.locationtech.jts.geom.MultiPoint; import org.locationtech.jts.geom.MultiPolygon; import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.geom.Puntal; import org.locationtech.jts.geom.impl.CoordinateArraySequence; import org.locationtech.jts.geom.impl.PackedCoordinateSequence; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import vector_tile.VectorTile; import vector_tile.VectorTile.Tile.GeomType; @@ -56,6 +59,8 @@ import vector_tile.VectorTile.Tile.GeomType; */ public class VectorTileEncoder { + private static final Logger LOGGER = LoggerFactory.getLogger(VectorTileEncoder.class); + private static final int EXTENT = 4096; private static final double SIZE = 256d; private static final double SCALE = ((double) EXTENT) / SIZE; @@ -92,7 +97,7 @@ public class VectorTileEncoder { return ((n >> 1) ^ (-(n & 1))); } - public static Geometry decode(byte geomTypeByte, int[] commands) { + public static Geometry decodeCommands(byte geomTypeByte, int[] commands) { VectorTile.Tile.GeomType geomType = Objects.requireNonNull(VectorTile.Tile.GeomType.forNumber(geomTypeByte)); GeometryFactory gf = GeoUtils.gf; int x = 0; @@ -259,7 +264,7 @@ public class VectorTileEncoder { Object value = values.get(feature.getTags(tagIdx++)); attrs.put(key, value); } - Geometry geometry = decode(feature.getType(), feature.getGeometryList()); + Geometry geometry = decodeCommands(feature.getType(), feature.getGeometryList()); features.add(new DecodedFeature( layerName, extent, @@ -272,11 +277,22 @@ public class VectorTileEncoder { return features; } - private static Geometry decode(GeomType type, List geometryList) { - return decode((byte) type.getNumber(), geometryList.stream().mapToInt(i -> i).toArray()); + private static Geometry decodeCommands(GeomType type, List geometryList) { + return decodeCommands((byte) type.getNumber(), geometryList.stream().mapToInt(i -> i).toArray()); } - public VectorTileEncoder addLayerFeatures(String layerName, List features) { + public interface VectorTileFeature { + + int[] commands(); + + long id(); + + byte geomType(); + + Map attrs(); + } + + public VectorTileEncoder addLayerFeatures(String layerName, List features) { if (features.isEmpty()) { return this; } @@ -287,7 +303,7 @@ public class VectorTileEncoder { layers.put(layerName, layer); } - for (LayerFeature inFeature : features) { + for (VectorTileFeature inFeature : features) { if (inFeature.commands().length > 0) { EncodedFeature outFeature = new EncodedFeature(inFeature); @@ -405,9 +421,11 @@ public class VectorTileEncoder { encode(lineString.getCoordinateSequence(), shouldClosePath(geometry)); } else if (geometry instanceof Point point) { encode(point.getCoordinateSequence(), false); - } else { + } else if (geometry instanceof Puntal) { encode(new CoordinateArraySequence(geometry.getCoordinates()), shouldClosePath(geometry), geometry instanceof MultiPoint); + } else { + LOGGER.warn("Unrecognized geometry type: " + geometry.getGeometryType()); } } @@ -483,7 +501,7 @@ public class VectorTileEncoder { private static final record EncodedFeature(IntArrayList tags, long id, byte geometryType, int[] geometry) { - EncodedFeature(LayerFeature in) { + EncodedFeature(VectorTileFeature in) { this(new IntArrayList(), in.id(), in.geomType(), in.commands()); } } diff --git a/src/test/java/com/onthegomap/flatmap/TestUtils.java b/src/test/java/com/onthegomap/flatmap/TestUtils.java new file mode 100644 index 00000000..8bd092c0 --- /dev/null +++ b/src/test/java/com/onthegomap/flatmap/TestUtils.java @@ -0,0 +1,47 @@ +package com.onthegomap.flatmap; + +import com.onthegomap.flatmap.geo.GeoUtils; +import java.util.ArrayList; +import java.util.List; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.MultiPoint; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.geom.util.AffineTransformation; + +public class TestUtils { + + public static final AffineTransformation TRANSFORM_TO_TILE = AffineTransformation + .scaleInstance(256d / 4096d, 256d / 4096d); + + public static List newCoordinateList(double... coords) { + List result = new ArrayList<>(coords.length / 2); + for (int i = 0; i < coords.length; i += 2) { + result.add(new Coordinate(coords[i], coords[i + 1])); + } + return result; + } + + public static Polygon newPolygon(double... coords) { + return GeoUtils.gf.createPolygon(newCoordinateList(coords).toArray(new Coordinate[0])); + } + + public static Point newPoint(double x, double y) { + return GeoUtils.gf.createPoint(new Coordinate(x, y)); + } + + public static MultiPoint newMultiPoint(Point... points) { + return GeoUtils.gf.createMultiPoint(points); + } + + public static MultiPolygon newMultiPolygon(Polygon... polys) { + return GeoUtils.gf.createMultiPolygon(polys); + } + + public static GeometryCollection newGeometryCollection(Geometry... geoms) { + return GeoUtils.gf.createGeometryCollection(geoms); + } +} diff --git a/src/test/java/com/onthegomap/flatmap/VectorTileEncoderTest.java b/src/test/java/com/onthegomap/flatmap/VectorTileEncoderTest.java index 59681677..eaf1e7f5 100644 --- a/src/test/java/com/onthegomap/flatmap/VectorTileEncoderTest.java +++ b/src/test/java/com/onthegomap/flatmap/VectorTileEncoderTest.java @@ -1,23 +1,246 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ package com.onthegomap.flatmap; +import static com.onthegomap.flatmap.TestUtils.TRANSFORM_TO_TILE; +import static com.onthegomap.flatmap.TestUtils.newGeometryCollection; +import static com.onthegomap.flatmap.TestUtils.newMultiPoint; +import static com.onthegomap.flatmap.TestUtils.newMultiPolygon; +import static com.onthegomap.flatmap.TestUtils.newPoint; +import static com.onthegomap.flatmap.TestUtils.newPolygon; import static com.onthegomap.flatmap.geo.GeoUtils.gf; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.google.common.primitives.Ints; import com.onthegomap.flatmap.VectorTileEncoder.DecodedFeature; +import com.onthegomap.flatmap.VectorTileEncoder.VectorTileFeature; import java.io.IOException; +import java.util.HashMap; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateXY; import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.MultiPolygon; import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.Polygon; +import vector_tile.VectorTile; import vector_tile.VectorTile.Tile.GeomType; +/** + * This class is copied from https://github.com/ElectronicChartCentre/java-vector-tile/blob/master/src/test/java/no/ecc/vectortile/VectorTileEncoderTest.java + * and modified based on the changes in VectorTileEncoder, and adapted to junit 5. + */ public class VectorTileEncoderTest { + // Tests adapted from https://github.com/ElectronicChartCentre/java-vector-tile/blob/master/src/test/java/no/ecc/vectortile/VectorTileEncoderTest.java + + private static List getCommands(Geometry geom) { + return Ints.asList(VectorTileEncoder.getCommands(TRANSFORM_TO_TILE.transform(geom))); + } + + @Test + public void testToGeomType() { + Geometry geometry = gf.createLineString(); + assertEquals(VectorTile.Tile.GeomType.LINESTRING, VectorTileEncoder.toGeomType(geometry)); + } + + @Test + public void testCommands() { + assertEquals(List.of(9, 6, 12, 18, 10, 12, 24, 44, 15), getCommands(newPolygon( + 3, 6, + 8, 12, + 20, 34, + 3, 6 + ))); + } + + @Test + public void testCommandsFilter() { + assertEquals(List.of(9, 6, 12, 18, 10, 12, 24, 44, 15), getCommands(newPolygon( + 3, 6, + 8, 12, + 8, 12, + 20, 34, + 3, 6 + ))); + } + + @Test + public void testPoint() { + assertEquals(List.of(9, 6, 12), getCommands(newMultiPoint( + newPoint(3, 6) + ))); + } + + @Test + public void testMultiPoint() { + assertEquals(List.of(17, 10, 14, 3, 9), getCommands(newMultiPoint( + newPoint(5, 7), + newPoint(3, 2) + ))); + } + + private static record SimpleVectorTileFeature( + int[] commands, + long id, + byte geomType, + Map attrs + ) implements VectorTileFeature { + + } + + private static VectorTileFeature newVectorTileFeature(Geometry geom, Map attrs) { + return new SimpleVectorTileFeature(VectorTileEncoder.getCommands(geom), 1, + (byte) VectorTileEncoder.toGeomType(geom).getNumber(), attrs); + } + + @Test + public void testNullAttributeValue() throws IOException { + VectorTileEncoder vtm = new VectorTileEncoder(); + Map attrs = new HashMap<>(); + attrs.put("key1", "value1"); + attrs.put("key2", null); + attrs.put("key3", "value3"); + + vtm.addLayerFeatures("DEPCNT", List.of( + newVectorTileFeature(newPoint(3, 6), attrs) + )); + + byte[] encoded = vtm.encode(); + assertNotSame(0, encoded.length); + + var decoded = VectorTileEncoder.decode(encoded); + assertEquals(List.of(new DecodedFeature("DEPCNT", 4096, newPoint(3, 6), Map.of( + "key1", "value1", + "key3", "value3" + ), 1)), decoded); + } + + @Test + public void testAttributeTypes() throws IOException { + VectorTileEncoder vtm = new VectorTileEncoder(); + + Map attrs = Map.of( + "key1", "value1", + "key2", 123, + "key3", 234.1f, + "key4", 567.123d, + "key5", (long) -123, + "key6", "value6", + "key7", Boolean.TRUE, + "key8", Boolean.FALSE + ); + + vtm.addLayerFeatures("DEPCNT", List.of(newVectorTileFeature(newPoint(3, 6), attrs))); + + byte[] encoded = vtm.encode(); + assertNotSame(0, encoded.length); + + List decoded = VectorTileEncoder.decode(encoded); + assertEquals(1, decoded.size()); + Map decodedAttributes = decoded.get(0).attributes(); + assertEquals("value1", decodedAttributes.get("key1")); + assertEquals(123L, decodedAttributes.get("key2")); + assertEquals(234.1f, decodedAttributes.get("key3")); + assertEquals(567.123d, decodedAttributes.get("key4")); + assertEquals((long) -123, decodedAttributes.get("key5")); + assertEquals("value6", decodedAttributes.get("key6")); + assertEquals(Boolean.TRUE, decodedAttributes.get("key7")); + assertEquals(Boolean.FALSE, decodedAttributes.get("key8")); + } + + @Test + public void testMultiPolygonCommands() { + // see https://github.com/mapbox/vector-tile-spec/blob/master/2.1/README.md + assertEquals(List.of( + 9, 0, 0, 26, 20, 0, 0, 20, 19, 0, 15, + 9, 22, 2, 26, 18, 0, 0, 18, 17, 0, 15, + 9, 4, 13, 26, 0, 8, 8, 0, 0, 7, 15 + ), getCommands(newMultiPolygon( + newPolygon(0, 0, + 10, 0, + 10, 10, + 0, 10, + 0, 0 + ), + newPolygon( + 11, 11, + 20, 11, + 20, 20, + 11, 20, + 11, 11 + ), + newPolygon( + 13, 13, + 13, 17, + 17, 17, + 17, 13, + 13, 13 + ) + ))); + } + + @Test + public void testMultiPolygon() throws IOException { + MultiPolygon mp = newMultiPolygon( + (Polygon) newPoint(13, 16).buffer(3), + (Polygon) newPoint(24, 25).buffer(5) + ); + assertTrue(mp.isValid()); + + Map attrs = Map.of("key1", "value1"); + + VectorTileEncoder vtm = new VectorTileEncoder(); + vtm.addLayerFeatures("mp", List.of(newVectorTileFeature(mp, attrs))); + + byte[] encoded = vtm.encode(); + assertTrue(encoded.length > 0); + + var features = VectorTileEncoder.decode(encoded); + assertEquals(1, features.size()); + MultiPolygon mp2 = (MultiPolygon) features.get(0).geometry(); + assertEquals(mp.getNumGeometries(), mp2.getNumGeometries()); + } + + @Test + public void testGeometryCollectionSilentlyIgnored() throws IOException { + GeometryCollection gc = newGeometryCollection( + newPoint(13, 16).buffer(3), + newPoint(24, 25) + ); + Map attributes = Map.of("key1", "value1"); + + VectorTileEncoder vtm = new VectorTileEncoder(); + vtm.addLayerFeatures("gc", List.of(newVectorTileFeature(gc, attributes))); + + byte[] encoded = vtm.encode(); + + var features = VectorTileEncoder.decode(encoded); + assertEquals(0, features.size()); + } + + // New tests added: @Test public void testRoundTripPoint() throws IOException { @@ -130,7 +353,7 @@ public class VectorTileEncoderTest { private void testRoundTrip(Geometry input, String layer, Map attrs, long id) throws IOException { int[] commands = VectorTileEncoder.getCommands(input); byte geomType = (byte) VectorTileEncoder.toGeomType(input).ordinal(); - Geometry output = VectorTileEncoder.decode(geomType, commands); + Geometry output = VectorTileEncoder.decodeCommands(geomType, commands); assertTrue(input.equalsExact(output), "\n" + input + "\n!=\n" + output); byte[] encoded = new VectorTileEncoder().addLayerFeatures(layer, List.of(