├── docs ├── mvt_build_flow.png └── mvt_read_flow.png ├── .gitignore ├── src ├── test │ ├── resources │ │ ├── vec_tile_test │ │ │ ├── 0 │ │ │ │ └── 0 │ │ │ │ │ └── 0.mvt │ │ │ └── game.mvt │ │ ├── mapbox │ │ │ └── vector_tile_js │ │ │ │ └── multi_poly_neg_exters.mvt │ │ └── wkt │ │ │ └── github_issue_27_01_multilinestring.wkt │ └── java │ │ └── com │ │ └── wdtinc │ │ └── mapbox_vector_tile │ │ ├── util │ │ └── ZigZagTest.java │ │ ├── encoding │ │ └── MvtUtilTest.java │ │ ├── adapt │ │ └── jts │ │ │ ├── model │ │ │ ├── JtsMvtTest.java │ │ │ └── JtsLayerTest.java │ │ │ ├── JtsAdapterIssue27Test.java │ │ │ ├── MvtEncoderTest.java │ │ │ └── MvtReaderTest.java │ │ └── build │ │ └── MvtBuildTest.java └── main │ ├── java │ └── com │ │ └── wdtinc │ │ └── mapbox_vector_tile │ │ ├── adapt │ │ └── jts │ │ │ ├── IGeometryFilter.java │ │ │ ├── TagIgnoreConverter.java │ │ │ ├── UserDataIgnoreConverter.java │ │ │ ├── ITagConverter.java │ │ │ ├── IUserDataConverter.java │ │ │ ├── RoundingFilter.java │ │ │ ├── TileGeomResult.java │ │ │ ├── GeomMinSizeFilter.java │ │ │ ├── MvtEncoder.java │ │ │ ├── model │ │ │ ├── JtsMvt.java │ │ │ └── JtsLayer.java │ │ │ ├── TagKeyValueMapConverter.java │ │ │ ├── UserDataKeyValueMapConverter.java │ │ │ ├── MvtReader.java │ │ │ └── JtsAdapter.java │ │ ├── encoding │ │ ├── ZigZag.java │ │ ├── MvtUtil.java │ │ ├── GeomCmd.java │ │ ├── GeomCmdHdr.java │ │ └── MvtValue.java │ │ ├── util │ │ ├── JdkUtils.java │ │ ├── Vec2d.java │ │ └── JtsGeomStats.java │ │ └── build │ │ ├── MvtLayerBuild.java │ │ ├── MvtLayerParams.java │ │ └── MvtLayerProps.java │ └── resources │ └── vector_tile.proto ├── .travis.yml ├── CONTRIBUTING.md ├── CHANGELOG.md ├── pom.xml ├── README.md └── LICENSE.txt /docs/mvt_build_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wdtinc/mapbox-vector-tile-java/HEAD/docs/mvt_build_flow.png -------------------------------------------------------------------------------- /docs/mvt_read_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wdtinc/mapbox-vector-tile-java/HEAD/docs/mvt_read_flow.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | dependency-reduced-pom.xml 4 | target/ 5 | *.aux.xml 6 | .DS_Store 7 | out/ 8 | 9 | -------------------------------------------------------------------------------- /src/test/resources/vec_tile_test/game.mvt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wdtinc/mapbox-vector-tile-java/HEAD/src/test/resources/vec_tile_test/game.mvt -------------------------------------------------------------------------------- /src/test/resources/vec_tile_test/0/0/0.mvt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wdtinc/mapbox-vector-tile-java/HEAD/src/test/resources/vec_tile_test/0/0/0.mvt -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | install: true 5 | script: mvn clean test 6 | cache: 7 | directories: 8 | - $HOME/.m2 9 | sudo: false -------------------------------------------------------------------------------- /src/test/resources/mapbox/vector_tile_js/multi_poly_neg_exters.mvt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wdtinc/mapbox-vector-tile-java/HEAD/src/test/resources/mapbox/vector_tile_js/multi_poly_neg_exters.mvt -------------------------------------------------------------------------------- /src/test/java/com/wdtinc/mapbox_vector_tile/util/ZigZagTest.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.util; 2 | 3 | import com.wdtinc.mapbox_vector_tile.encoding.ZigZag; 4 | import org.junit.Test; 5 | 6 | import static org.junit.Assert.*; 7 | 8 | /** 9 | * Test zig zag encoding function. 10 | */ 11 | public final class ZigZagTest { 12 | 13 | @Test 14 | public void encodeAndDecode() { 15 | assertEquals(ZigZag.decode(ZigZag.encode(0)), 0); 16 | assertEquals(ZigZag.decode(ZigZag.encode(10018754)), 10018754); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing/Development 2 | 3 | Feedback and pull requests are welcome. See below for details. 4 | 5 | ## Pull Request Guidelines 6 | 7 | When creating a pull request: 8 | 9 | * Fork the repository 10 | * Create a separate branch for the feature or issue-fix 11 | 12 | Additionally, code should be: 13 | 14 | * Tested 15 | * Written clearly and contain documentation 16 | * Matching existing code style 17 | 18 | ## Committers 19 | 20 | The list of people with committer access will be kept in the developer section of the POM.xml. 21 | -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/adapt/jts/IGeometryFilter.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.adapt.jts; 2 | 3 | import org.locationtech.jts.geom.Geometry; 4 | 5 | public interface IGeometryFilter { 6 | 7 | /** 8 | * Return true if the value should be accepted (pass), or false if the value should be rejected (fail). 9 | * 10 | * @param geometry input to test 11 | * @return true if the value should be accepted (pass), or false if the value should be rejected (fail) 12 | * @see Geometry 13 | */ 14 | boolean accept(Geometry geometry); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/adapt/jts/TagIgnoreConverter.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.adapt.jts; 2 | 3 | import com.wdtinc.mapbox_vector_tile.VectorTile; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Ignores tags, always returns null. 9 | * 10 | * @see ITagConverter 11 | */ 12 | public final class TagIgnoreConverter implements ITagConverter { 13 | @Override 14 | public Object toUserData(Long id, List tags, List keysList, 15 | List valuesList) { 16 | return null; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/adapt/jts/UserDataIgnoreConverter.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.adapt.jts; 2 | 3 | import com.wdtinc.mapbox_vector_tile.VectorTile; 4 | import com.wdtinc.mapbox_vector_tile.build.MvtLayerProps; 5 | 6 | /** 7 | * Ignores user data, does not take any action. 8 | * 9 | * @see IUserDataConverter 10 | */ 11 | public final class UserDataIgnoreConverter implements IUserDataConverter { 12 | @Override 13 | public void addTags(Object userData, MvtLayerProps layerProps, 14 | VectorTile.Tile.Feature.Builder featureBuilder) { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/resources/wkt/github_issue_27_01_multilinestring.wkt: -------------------------------------------------------------------------------- 1 | MULTILINESTRING ((545022.4644456336 6867178.738599846, 545023.7891475739 6867185.429774573, 545024.813286887 6867190.64415437, 545029.8894556686 6867190.206583908, 545036.2791944387 6867192.339740138, 545040.0751890754 6867195.7309127385, 545042.7913846518 6867201.255245361, 545043.1476070203 6867206.34201043, 545042.4574261782 6867209.477938888, 545040.03066128 6867213.944815833, 545037.4591810413 6867216.4608537285, 545032.939609716 6867218.794570745, 545028.5536217794 6867219.469160945, 545023.5331127449 6867218.5757847475, 545020.4606947971 6867217.026050759, 545015.5960330492 6867211.611100222, 545014.0486921292 6867206.761349631, 545014.1488796688 6867202.185083897, 545016.5088528739 6867196.4966615895, 545020.0822085281 6867192.868471254, 545024.813286887 6867190.64415437)) -------------------------------------------------------------------------------- /src/test/java/com/wdtinc/mapbox_vector_tile/encoding/MvtUtilTest.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.encoding; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Test MVT utility functions. 9 | */ 10 | public final class MvtUtilTest { 11 | 12 | @Test 13 | public void testHeaders() { 14 | assertEquals(GeomCmdHdr.cmdHdr(GeomCmd.MoveTo, 1), 9); 15 | assertEquals(GeomCmdHdr.cmdHdr(GeomCmd.MoveTo, 1) >> 3, 1); 16 | 17 | assertEquals(GeomCmdHdr.getCmdId(GeomCmdHdr.cmdHdr(GeomCmd.MoveTo, 1)), GeomCmd.MoveTo.getCmdId()); 18 | assertEquals(GeomCmdHdr.getCmdLength(GeomCmdHdr.cmdHdr(GeomCmd.MoveTo, 1)), 1); 19 | 20 | for (GeomCmd c : GeomCmd.values()) { 21 | assertEquals(GeomCmdHdr.cmdHdr(c, 1) & 0x7, c.getCmdId()); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/adapt/jts/ITagConverter.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.adapt.jts; 2 | 3 | import com.wdtinc.mapbox_vector_tile.VectorTile; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Process MVT tags and feature id, convert to user data object. The returned user data 9 | * object may be null. 10 | */ 11 | public interface ITagConverter { 12 | 13 | /** 14 | * Convert MVT user data to JTS user data object or null. 15 | * 16 | * @param id feature id, may be {@code null} 17 | * @param tags MVT feature tags, may be invalid 18 | * @param keysList layer key list 19 | * @param valuesList layer value list 20 | * @return user data object or null 21 | */ 22 | Object toUserData(Long id, 23 | List tags, 24 | List keysList, 25 | List valuesList); 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/encoding/ZigZag.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.encoding; 2 | 3 | /** 4 | * See: Google Protocol Buffers Docs 5 | */ 6 | public final class ZigZag { 7 | 8 | /** 9 | * See: Google Protocol Buffers Docs 10 | * 11 | * @param n integer to encode 12 | * @return zig-zag encoded integer 13 | */ 14 | public static int encode(int n) { 15 | return (n << 1) ^ (n >> 31); 16 | } 17 | 18 | /** 19 | * See: Google Protocol Buffers Docs 20 | * 21 | * @param n zig-zag encoded integer to decode 22 | * @return decoded integer 23 | */ 24 | public static int decode(int n) { 25 | return (n >> 1) ^ (-(n & 1)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/adapt/jts/IUserDataConverter.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.adapt.jts; 2 | 3 | import com.wdtinc.mapbox_vector_tile.VectorTile; 4 | import com.wdtinc.mapbox_vector_tile.build.MvtLayerProps; 5 | 6 | /** 7 | * Processes a user data object, converts to MVT feature tags. 8 | */ 9 | public interface IUserDataConverter { 10 | 11 | /** 12 | *

Convert user data to MVT tags. The supplied user data may be null. Implementation 13 | * should update layerProps and optionally set the feature id.

14 | * 15 | *

SIDE EFFECT: The implementation may add tags to featureBuilder, modify layerProps, modify userData.

16 | * 17 | * @param userData user object may contain values in any format; may be null 18 | * @param layerProps properties global to the layer the feature belongs to 19 | * @param featureBuilder may be modified to contain additional tags 20 | */ 21 | void addTags(Object userData, MvtLayerProps layerProps, VectorTile.Tile.Feature.Builder featureBuilder); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/adapt/jts/RoundingFilter.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.adapt.jts; 2 | 3 | import org.locationtech.jts.geom.CoordinateSequence; 4 | import org.locationtech.jts.geom.CoordinateSequenceFilter; 5 | 6 | /** 7 | *

Round each coordinate value to an integer.

8 | * 9 | *

Mapbox vector tiles have fixed precision. This filter can be useful for reducing precision to 10 | * the extent of a MVT.

11 | */ 12 | public final class RoundingFilter implements CoordinateSequenceFilter { 13 | 14 | public static final RoundingFilter INSTANCE = new RoundingFilter(); 15 | 16 | private RoundingFilter() {} 17 | 18 | @Override 19 | public void filter(CoordinateSequence seq, int i) { 20 | seq.setOrdinate(i, 0, Math.round(seq.getOrdinate(i, 0))); 21 | seq.setOrdinate(i, 1, Math.round(seq.getOrdinate(i, 1))); 22 | } 23 | 24 | @Override 25 | public boolean isDone() { 26 | return false; 27 | } 28 | 29 | @Override 30 | public boolean isGeometryChanged() { 31 | return true; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/encoding/MvtUtil.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.encoding; 2 | 3 | import com.wdtinc.mapbox_vector_tile.VectorTile; 4 | 5 | /** 6 | *

Useful misc operations for encoding 'Mapbox Vector Tiles'.

7 | * 8 | *

See: https://github.com/mapbox/vector-tile-spec

9 | */ 10 | public final class MvtUtil { 11 | 12 | /** 13 | * Return whether the MVT geometry type should be closed with a {@link GeomCmd#ClosePath}. 14 | * 15 | * @param geomType the type of MVT geometry 16 | * @return true if the geometry should be closed, false if it should not be closed 17 | */ 18 | public static boolean shouldClosePath(VectorTile.Tile.GeomType geomType) { 19 | final boolean closeReq; 20 | 21 | switch(geomType) { 22 | case POLYGON: 23 | closeReq = true; 24 | break; 25 | default: 26 | closeReq = false; 27 | break; 28 | } 29 | 30 | return closeReq; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/adapt/jts/TileGeomResult.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.adapt.jts; 2 | 3 | import org.locationtech.jts.geom.Envelope; 4 | import org.locationtech.jts.geom.Geometry; 5 | import org.locationtech.jts.geom.GeometryFactory; 6 | import com.wdtinc.mapbox_vector_tile.build.MvtLayerParams; 7 | import com.wdtinc.mapbox_vector_tile.util.JdkUtils; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * Processing result containing intersection geometry and MVT geometry. 13 | * 14 | * @see JtsAdapter#createTileGeom(Geometry, Envelope, GeometryFactory, MvtLayerParams, IGeometryFilter) 15 | */ 16 | public final class TileGeomResult { 17 | 18 | /** 19 | * Intersection geometry (projection units and coordinates). 20 | */ 21 | public final List intGeoms; 22 | 23 | /** 24 | * Geometry in MVT coordinates (tile extent units, screen coordinates). 25 | */ 26 | public final List mvtGeoms; 27 | 28 | /** 29 | * Create TileGeomResult, which contains the intersection of geometry and MVT geometry. 30 | * 31 | * @param intGeoms geometry intersecting tile 32 | * @param mvtGeoms geometry for MVT 33 | * @throws NullPointerException if intGeoms or mvtGeoms are null 34 | */ 35 | public TileGeomResult(List intGeoms, List mvtGeoms) { 36 | JdkUtils.requireNonNull(intGeoms); 37 | JdkUtils.requireNonNull(mvtGeoms); 38 | this.intGeoms = intGeoms; 39 | this.mvtGeoms = mvtGeoms; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/util/JdkUtils.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.util; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * Mimic future JDK capabilities for backwards compatibility. 7 | */ 8 | public final class JdkUtils { 9 | 10 | private JdkUtils() {} 11 | 12 | /** 13 | * Backwards compatible {@link Map#putIfAbsent} to support < Android API 24. 14 | * 15 | * If the specified key is not already associated with a value (or is mapped 16 | * to {@code null}) associates it with the given value and returns 17 | * {@code null}, else returns the current value. 18 | * 19 | * @param key key with which the specified value is to be associated 20 | * @param value value to be associated with the specified key 21 | * @return the previous value associated with the specified key, or 22 | * {@code null} if there was no mapping for the key. 23 | */ 24 | public static V putIfAbsent(Map map, K key, V value) { 25 | V val = map.get(key); 26 | if (val == null) { 27 | val = map.put(key, value); 28 | } 29 | return val; 30 | } 31 | 32 | /** 33 | * This method mimics the behavior of Objects.requireNonNull method to allow 34 | * Android API level 15 backward compatibility. 35 | * 36 | * @param object 37 | * @return object 38 | * @throws NullPointerException if object is null 39 | */ 40 | public static T requireNonNull(T object) { 41 | if (object == null) 42 | throw new NullPointerException(); 43 | return object; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/encoding/GeomCmd.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.encoding; 2 | 3 | /** 4 | * MVT draw command types. 5 | * 6 | * @see GeomCmdHdr 7 | */ 8 | public enum GeomCmd { 9 | MoveTo(1, 2), 10 | LineTo(2, 2), 11 | ClosePath(7, 0); 12 | 13 | /** 14 | * Unique command ID. 15 | */ 16 | private final int cmdId; 17 | 18 | /** 19 | * Amount of parameters that follow the command. 20 | */ 21 | private final int paramCount; 22 | 23 | GeomCmd(int cmdId, int paramCount) { 24 | this.cmdId = cmdId; 25 | this.paramCount = paramCount; 26 | } 27 | 28 | /** 29 | * @return unique command ID. 30 | */ 31 | public int getCmdId() { 32 | return cmdId; 33 | } 34 | 35 | /** 36 | * @return amount of parameters that follow the command. 37 | */ 38 | public int getParamCount() { 39 | return paramCount; 40 | } 41 | 42 | 43 | /** 44 | * Return matching {@link GeomCmd} for the provided cmdId, or null if there is not 45 | * a matching command. 46 | * 47 | * @param cmdId command id to find match for 48 | * @return command with matching id, or null if there is not a matching command 49 | */ 50 | public static GeomCmd fromId(int cmdId) { 51 | final GeomCmd geomCmd; 52 | switch (cmdId) { 53 | case 1: 54 | geomCmd = MoveTo; 55 | break; 56 | case 2: 57 | geomCmd = LineTo; 58 | break; 59 | case 7: 60 | geomCmd = ClosePath; 61 | break; 62 | default: 63 | geomCmd = null; 64 | } 65 | return geomCmd; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/adapt/jts/GeomMinSizeFilter.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.adapt.jts; 2 | 3 | import org.locationtech.jts.geom.*; 4 | 5 | /** 6 | * Filter {@link Polygon} and {@link MultiPolygon} by area or 7 | * {@link LineString} and {@link MultiLineString} by length. 8 | * 9 | * @see IGeometryFilter 10 | */ 11 | public final class GeomMinSizeFilter implements IGeometryFilter { 12 | 13 | /** 14 | * Minimum area. 15 | */ 16 | private final double minArea; 17 | 18 | /** 19 | * Minimum length. 20 | */ 21 | private final double minLength; 22 | 23 | /** 24 | * GeomMinSizeFilter. 25 | * @param minArea minimum area required for a {@link Polygon} or {@link MultiPolygon} 26 | * @param minLength minimum length required for a {@link LineString} or {@link MultiLineString} 27 | */ 28 | public GeomMinSizeFilter(double minArea, double minLength) { 29 | if(minArea < 0.0d) { 30 | throw new IllegalArgumentException("minArea must be >= 0"); 31 | } 32 | if(minLength < 0.0d) { 33 | throw new IllegalArgumentException("minLength must be >= 0"); 34 | } 35 | 36 | this.minArea = minArea; 37 | this.minLength = minLength; 38 | } 39 | 40 | @Override 41 | public boolean accept(Geometry geometry) { 42 | boolean accept = true; 43 | 44 | if((geometry instanceof Polygon || geometry instanceof MultiPolygon) 45 | && geometry.getArea() < minArea) { 46 | accept = false; 47 | 48 | } else if((geometry instanceof LineString || geometry instanceof MultiLineString) 49 | && geometry.getLength() < minLength) { 50 | accept = false; 51 | } 52 | 53 | return accept; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/build/MvtLayerBuild.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.build; 2 | 3 | import com.wdtinc.mapbox_vector_tile.VectorTile; 4 | import com.wdtinc.mapbox_vector_tile.encoding.MvtValue; 5 | 6 | /** 7 | * Utility methods for building Mapbox-Vector-Tile layers. 8 | */ 9 | public final class MvtLayerBuild { 10 | 11 | /** 12 | * Create a new {@link com.wdtinc.mapbox_vector_tile.VectorTile.Tile.Layer.Builder} instance with 13 | * initialized version, name, and extent metadata. 14 | * 15 | * @param layerName name of the layer 16 | * @param mvtLayerParams tile creation parameters 17 | * @return new layer builder instance with initialized metadata. 18 | */ 19 | public static VectorTile.Tile.Layer.Builder newLayerBuilder(String layerName, 20 | MvtLayerParams mvtLayerParams) { 21 | final VectorTile.Tile.Layer.Builder layerBuilder = VectorTile.Tile.Layer.newBuilder(); 22 | layerBuilder.setVersion(2); 23 | layerBuilder.setName(layerName); 24 | layerBuilder.setExtent(mvtLayerParams.extent); 25 | 26 | return layerBuilder; 27 | } 28 | 29 | /** 30 | * Modifies {@code layerBuilder} to contain properties from {@code layerProps}. 31 | * 32 | * @param layerBuilder layer builder to write to 33 | * @param layerProps properties to write 34 | */ 35 | public static void writeProps(VectorTile.Tile.Layer.Builder layerBuilder, 36 | MvtLayerProps layerProps) { 37 | 38 | // Add keys 39 | layerBuilder.addAllKeys(layerProps.getKeys()); 40 | 41 | // Add values 42 | final Iterable vals = layerProps.getVals(); 43 | for (Object val : vals) { 44 | layerBuilder.addValues(MvtValue.toValue(val)); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/com/wdtinc/mapbox_vector_tile/adapt/jts/model/JtsMvtTest.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.adapt.jts.model; 2 | 3 | import org.junit.Test; 4 | 5 | import static java.util.Arrays.asList; 6 | import static org.junit.Assert.*; 7 | 8 | public final class JtsMvtTest { 9 | 10 | @Test 11 | public void testConstructor() { 12 | final JtsLayer layer1 = new JtsLayer("first"); 13 | final JtsLayer layer2 = new JtsLayer("second"); 14 | 15 | final JtsMvt mvt = new JtsMvt(layer1, layer2); 16 | assertTrue(mvt.getLayers().containsAll(asList(layer1, layer2))); 17 | } 18 | 19 | @Test 20 | public void testLayerByName() { 21 | final JtsLayer layer1 = new JtsLayer("first"); 22 | final JtsLayer layer2 = new JtsLayer("second"); 23 | 24 | final JtsMvt mvt = new JtsMvt(layer1, layer2); 25 | 26 | assertEquals(layer1, mvt.getLayer("first")); 27 | assertEquals(layer2, mvt.getLayer("second")); 28 | } 29 | 30 | @Test 31 | public void testEquality() { 32 | final JtsLayer layer1 = new JtsLayer("first"); 33 | final JtsLayer layer2 = new JtsLayer("second"); 34 | 35 | final JtsMvt mvt = new JtsMvt(layer1, layer2); 36 | 37 | final JtsLayer duplicateLayer1 = new JtsLayer("first"); 38 | final JtsLayer duplicateLayer2 = new JtsLayer("second"); 39 | 40 | final JtsMvt mvt2 = new JtsMvt(duplicateLayer1, duplicateLayer2); 41 | 42 | assertTrue(mvt.equals(mvt2)); 43 | 44 | final JtsMvt mvt3 = new JtsMvt(duplicateLayer1, duplicateLayer2, new JtsLayer("extra")); 45 | 46 | assertFalse(mvt.equals(mvt3)); 47 | } 48 | 49 | @Test 50 | public void testNoSuchLayer() { 51 | final JtsLayer layer = new JtsLayer("example"); 52 | final JtsMvt mvt = new JtsMvt(layer); 53 | 54 | assertNull(mvt.getLayer("No Such Layer")); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/build/MvtLayerParams.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.build; 2 | 3 | 4 | /** 5 | * Immutable parameters collection for Mapbox-Vector-Tile creation. 6 | */ 7 | public final class MvtLayerParams { 8 | 9 | /** 10 | * Default layer parameters created using {@link #MvtLayerParams()}. 11 | */ 12 | public static final MvtLayerParams DEFAULT = new MvtLayerParams(); 13 | 14 | 15 | /** 16 | * the resolution of the tile in 'pixel' dimensions. 17 | */ 18 | public final int tileSize; 19 | 20 | /** 21 | * the resolution of the MVT local coordinate system. 22 | */ 23 | public final int extent; 24 | 25 | /** 26 | * ratio of tile 'pixel' dimensions to tile extent dimensions. 27 | */ 28 | public final float ratio; 29 | 30 | /** 31 | * Construct default layer sizing parameters for MVT creation. 32 | * 33 | *

Uses defaults:

34 | *
    35 | *
  • {@link #tileSize} = 256
  • 36 | *
  • {@link #extent} = 4096
  • 37 | *
38 | * 39 | * @see #MvtLayerParams(int, int) 40 | */ 41 | public MvtLayerParams() { 42 | this(256, 4096); 43 | } 44 | 45 | /** 46 | * Construct layer sizing parameters for MVT creation. 47 | * 48 | * @param tileSize the resolution of the tile in pixel coordinates, must be > 0 49 | * @param extent the resolution of the MVT local coordinate system, must be > 0 50 | */ 51 | public MvtLayerParams(int tileSize, int extent) { 52 | if(tileSize <= 0) { 53 | throw new IllegalArgumentException("tileSize must be > 0"); 54 | } 55 | 56 | if(extent <= 0) { 57 | throw new IllegalArgumentException("extent must be > 0"); 58 | } 59 | 60 | this.tileSize = tileSize; 61 | this.extent = extent; 62 | this.ratio = extent / (float) tileSize; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## 3.1.1 (August 1 2019) 3 | 4 | #### Fixes 5 | 6 | - (27) Fix multiline string conversion to MVT feature where final segment could be missing. 7 | 8 | - (36) Polygon orientation (winding order) 9 | 10 | - (38) Support PackedCoordinateSequence in MvtReader 11 | 12 | ## 3.1.0 (April 12 2019) 13 | 14 | #### Features 15 | 16 | - Add extent property to JtsLayer when reading an MVT from MvtReader. Does not effect the extent when writing a JtsLayer unless passed as an option in MvtLayerParams. 17 | 18 | ## 3.0.2 (April 2 2019) 19 | 20 | #### Features 21 | 22 | - Upgrade to JTS 1.15.1 from JTS 1.15.0 23 | 24 | ## 3.0.0 (Jan 12 2018) 25 | 26 | #### Features 27 | 28 | - Android API Level 15 Compatibility 29 | 30 | - BREAKING CHANGE: Use File rather than Path for Android compatibility. 31 | 32 | - BREAKING CHANGE: Dependency for JTS to 1.15 under locationtech (Eclipse License). 33 | 34 | - Dependency change - protobuf-java to 3.5.1 from 3.0.0-beta-2. 35 | 36 | - Dependency change - JUnit to 4.12 from 4.8.2. 37 | 38 | - Dependency change - org.slf4j to 1.7.25 from 1.7.12. 39 | 40 | #### Fixes 41 | 42 | - Fixed guard cases in JTSAdapter to return an empty list when the geometry was not valid for encoding. 43 | 44 | - Fixed calculation to float from int of MvtLayerParams#ratio. This value was not being read within the project but may affect other projects. 45 | 46 | ## 2.0.0 (Oct 2 2017) 47 | 48 | #### Features 49 | 50 | - BREAKING CHANGE: Rework MVTReader to return JtsMvt objects that retain MVT layer information. No longer returns flat collection of JTS Geometry. 51 | 52 | - TagKeyValueMapConverter now uses a LinkedHashMap internally to preserve property order. 53 | 54 | ## 1.2.0 (Jul 6 2017) 55 | 56 | #### Features 57 | 58 | - Add support for clipping outside of MVT extent. 59 | 60 | ## 1.1.1 (Nov 1 2016) 61 | 62 | #### Fixes 63 | 64 | - Fix issue with JtsAdapter#flatFeatureList(Geometry) using wrong count for flattening GeometryCollection. 65 | 66 | ## 1.1.0 (Sep 2 2016) 67 | 68 | #### Features 69 | 70 | - Add support for other Polygon and Multipolygon ring classification when reading in a MVT. 71 | 72 | 73 | ## 1.0 (Aug 12 2016) 74 | 75 | - Initial release. -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/encoding/GeomCmdHdr.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.encoding; 2 | 3 | 4 | /** 5 | * Utilities for working with geometry command headers. 6 | * 7 | * @see GeomCmd 8 | */ 9 | public final class GeomCmdHdr { 10 | 11 | private static int CLOSE_PATH_HDR = cmdHdr(GeomCmd.ClosePath, 1); 12 | 13 | /** 14 | *

Encodes a 'command header' with the first 3 LSB as the command id, the remaining bits 15 | * as the command length. See the vector-tile-spec for details.

16 | * 17 | * @param cmd command to execute 18 | * @param length how many times the command is repeated 19 | * @return encoded 'command header' integer 20 | */ 21 | public static int cmdHdr(GeomCmd cmd, int length) { 22 | return (cmd.getCmdId() & 0x7) | (length << 3); 23 | } 24 | 25 | /** 26 | * Get the length component from the 'command header' integer. 27 | * 28 | * @param cmdHdr encoded 'command header' integer 29 | * @return command length 30 | */ 31 | public static int getCmdLength(int cmdHdr) { 32 | return cmdHdr >> 3; 33 | } 34 | 35 | /** 36 | * Get the id component from the 'command header' integer. 37 | * 38 | * @param cmdHdr encoded 'command header' integer 39 | * @return command id 40 | */ 41 | public static int getCmdId(int cmdHdr) { 42 | return cmdHdr & 0x7; 43 | } 44 | 45 | /** 46 | * Get the id component from the 'command header' integer, then find the 47 | * {@link GeomCmd} with a matching id. 48 | * 49 | * @param cmdHdr encoded 'command header' integer 50 | * @return command with matching id, or null if a match could not be made 51 | */ 52 | public static GeomCmd getCmd(int cmdHdr) { 53 | final int cmdId = getCmdId(cmdHdr); 54 | return GeomCmd.fromId(cmdId); 55 | } 56 | 57 | /** 58 | * @return encoded 'command header' integer for {@link GeomCmd#ClosePath}. 59 | */ 60 | public static int closePathCmdHdr() { 61 | return CLOSE_PATH_HDR; 62 | } 63 | 64 | /** 65 | * Maximum allowed 'command header' length value. 66 | */ 67 | public static final int CMD_HDR_LEN_MAX = (int) (Math.pow(2, 29) - 1); 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/build/MvtLayerProps.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.build; 2 | 3 | import com.wdtinc.mapbox_vector_tile.encoding.MvtValue; 4 | import com.wdtinc.mapbox_vector_tile.util.JdkUtils; 5 | 6 | import java.util.*; 7 | 8 | /** 9 | * Support MVT features that must reference properties by their key and value index. 10 | */ 11 | public final class MvtLayerProps { 12 | private LinkedHashMap keys; 13 | private LinkedHashMap vals; 14 | 15 | public MvtLayerProps() { 16 | keys = new LinkedHashMap<>(); 17 | vals = new LinkedHashMap<>(); 18 | } 19 | 20 | public Integer keyIndex(String k) { 21 | return keys.get(k); 22 | } 23 | 24 | public Integer valueIndex(Object v) { 25 | return vals.get(v); 26 | } 27 | 28 | /** 29 | * Add the key and return it's index code. If the key already is present, the previous 30 | * index code is returned and no insertion is done. 31 | * 32 | * @param key key to add 33 | * @return index of the key 34 | */ 35 | public int addKey(String key) { 36 | JdkUtils.requireNonNull(key); 37 | int nextIndex = keys.size(); 38 | final Integer mapIndex = JdkUtils.putIfAbsent(keys, key, nextIndex); 39 | return mapIndex == null ? nextIndex : mapIndex; 40 | } 41 | 42 | /** 43 | * Add the value and return it's index code. If the value already is present, the previous 44 | * index code is returned and no insertion is done. If {@code value} is an unsupported type 45 | * for encoding in a MVT, then it will not be added. 46 | * 47 | * @param value value to add 48 | * @return index of the value, -1 on unsupported value types 49 | * @see MvtValue#isValidPropValue(Object) 50 | */ 51 | public int addValue(Object value) { 52 | JdkUtils.requireNonNull(value); 53 | if(!MvtValue.isValidPropValue(value)) { 54 | return -1; 55 | } 56 | 57 | int nextIndex = vals.size(); 58 | final Integer mapIndex = JdkUtils.putIfAbsent(vals, value, nextIndex); 59 | return mapIndex == null ? nextIndex : mapIndex; 60 | } 61 | 62 | public Iterable getKeys() { 63 | return keys.keySet(); 64 | } 65 | 66 | public Iterable getVals() { 67 | return vals.keySet(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/adapt/jts/MvtEncoder.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.adapt.jts; 2 | 3 | import org.locationtech.jts.geom.Geometry; 4 | import com.wdtinc.mapbox_vector_tile.VectorTile; 5 | import com.wdtinc.mapbox_vector_tile.adapt.jts.model.JtsLayer; 6 | import com.wdtinc.mapbox_vector_tile.adapt.jts.model.JtsMvt; 7 | import com.wdtinc.mapbox_vector_tile.build.MvtLayerBuild; 8 | import com.wdtinc.mapbox_vector_tile.build.MvtLayerParams; 9 | import com.wdtinc.mapbox_vector_tile.build.MvtLayerProps; 10 | 11 | import java.util.Collection; 12 | import java.util.List; 13 | 14 | /** 15 | * Convenience class allows easy encoding of a {@link JtsMvt} to bytes. 16 | */ 17 | public final class MvtEncoder { 18 | 19 | /** 20 | * Encode a {@link JtsMvt} to byte[] ready for writing to a file. 21 | * 22 | *

Uses {@link MvtLayerParams#DEFAULT} and {@link UserDataKeyValueMapConverter} to transform the JtsMvt.

23 | * 24 | * @param mvt input to encode to bytes 25 | * @return bytes ready for writing to a .mvt 26 | * @see #encode(JtsMvt, MvtLayerParams, IUserDataConverter) 27 | */ 28 | public static byte[] encode(JtsMvt mvt) { 29 | return encode(mvt, MvtLayerParams.DEFAULT, new UserDataKeyValueMapConverter()); 30 | } 31 | 32 | /** 33 | * Encode a {@link JtsMvt} to byte[] ready for writing to a file. 34 | * 35 | * @param mvt input to encode to bytes 36 | * @param mvtLayerParams tile creation parameters 37 | * @param userDataConverter converts {@link Geometry#userData} to MVT feature tags 38 | * @return bytes ready for writing to a .mvt 39 | */ 40 | public static byte[] encode(JtsMvt mvt, MvtLayerParams mvtLayerParams, IUserDataConverter userDataConverter) { 41 | 42 | // Build MVT 43 | final VectorTile.Tile.Builder tileBuilder = VectorTile.Tile.newBuilder(); 44 | 45 | for(JtsLayer layer : mvt.getLayers()) { 46 | final Collection layerGeoms = layer.getGeometries(); 47 | 48 | // Create MVT layer 49 | final VectorTile.Tile.Layer.Builder layerBuilder = 50 | MvtLayerBuild.newLayerBuilder(layer.getName(), mvtLayerParams); 51 | final MvtLayerProps layerProps = new MvtLayerProps(); 52 | 53 | // MVT tile geometry to MVT features 54 | final List features = JtsAdapter.toFeatures( 55 | layerGeoms, layerProps, userDataConverter); 56 | layerBuilder.addAllFeatures(features); 57 | MvtLayerBuild.writeProps(layerBuilder, layerProps); 58 | 59 | // Build MVT layer 60 | final VectorTile.Tile.Layer vtl = layerBuilder.build(); 61 | tileBuilder.addLayers(vtl); 62 | } 63 | 64 | // Build MVT 65 | return tileBuilder.build().toByteArray(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/adapt/jts/model/JtsMvt.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.adapt.jts.model; 2 | 3 | import java.util.*; 4 | 5 | /** 6 | * JTS model of a Mapbox Vector Tile. 7 | */ 8 | public class JtsMvt { 9 | 10 | /** 11 | * Map layers by name. 12 | */ 13 | private final Map layersByName; 14 | 15 | /** 16 | * Create an empty MVT. 17 | */ 18 | public JtsMvt() { 19 | this(Collections.emptyList()); 20 | } 21 | 22 | /** 23 | * Create MVT with single layer. 24 | * 25 | * @param layer single MVT layer 26 | */ 27 | public JtsMvt(JtsLayer layer) { 28 | this(Collections.singletonList(layer)); 29 | } 30 | 31 | /** 32 | * Create MVT with the provided layers. 33 | * 34 | * @param layers multiple MVT layers 35 | */ 36 | public JtsMvt(JtsLayer... layers) { 37 | this(new ArrayList<>(Arrays.asList(layers))); 38 | } 39 | 40 | /** 41 | * Create a MVT with the provided layers. 42 | * 43 | * @param layers multiple MVT layers 44 | */ 45 | public JtsMvt(Collection layers) { 46 | 47 | // Linked hash map to preserve ordering 48 | layersByName = new LinkedHashMap<>(layers.size()); 49 | 50 | for(JtsLayer nextLayer : layers) { 51 | layersByName.put(nextLayer.getName(), nextLayer); 52 | } 53 | } 54 | 55 | /** 56 | * Get the layer by the given name. 57 | * 58 | * @param name layer name 59 | * @return layer with matching name, or null if none exists 60 | */ 61 | public JtsLayer getLayer(String name) { 62 | return layersByName.get(name); 63 | } 64 | 65 | /** 66 | * Get all layers within the vector tile mapped by name. 67 | * 68 | * @return mapping of layer name to layer 69 | */ 70 | public Map getLayersByName() { 71 | return layersByName; 72 | } 73 | 74 | /** 75 | * Get get all layers within the vector tile. 76 | * 77 | * @return insertion-ordered collection of layers 78 | */ 79 | public Collection getLayers() { 80 | return layersByName.values(); 81 | } 82 | 83 | @Override 84 | public String toString() { 85 | return "JtsMvt{" + 86 | "layersByName=" + layersByName + 87 | '}'; 88 | } 89 | 90 | @Override 91 | public boolean equals(Object o) { 92 | if (this == o) return true; 93 | if (o == null || getClass() != o.getClass()) return false; 94 | 95 | JtsMvt jtsMvt = (JtsMvt) o; 96 | 97 | return layersByName.equals(jtsMvt.layersByName); 98 | } 99 | 100 | @Override 101 | public int hashCode() { 102 | return layersByName.hashCode(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/test/java/com/wdtinc/mapbox_vector_tile/adapt/jts/JtsAdapterIssue27Test.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.adapt.jts; 2 | 3 | import com.wdtinc.mapbox_vector_tile.VectorTile; 4 | import com.wdtinc.mapbox_vector_tile.adapt.jts.model.JtsMvt; 5 | import com.wdtinc.mapbox_vector_tile.build.MvtLayerBuild; 6 | import com.wdtinc.mapbox_vector_tile.build.MvtLayerParams; 7 | import com.wdtinc.mapbox_vector_tile.build.MvtLayerProps; 8 | import org.locationtech.jts.geom.Envelope; 9 | import org.locationtech.jts.geom.Geometry; 10 | import org.locationtech.jts.geom.GeometryFactory; 11 | import org.locationtech.jts.io.ParseException; 12 | import org.locationtech.jts.io.WKTReader; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import java.io.File; 16 | import java.io.FileReader; 17 | import java.io.IOException; 18 | import java.nio.file.Files; 19 | import java.util.List; 20 | 21 | /** 22 | * Manual test for ensuring linestring geometry does not miss segments on the end. 23 | */ 24 | public class JtsAdapterIssue27Test { 25 | 26 | public static void main(String args[]) throws IOException, ParseException { 27 | testLineStringMisssingEndSegments(); 28 | } 29 | 30 | public static void testLineStringMisssingEndSegments() throws IOException, ParseException { 31 | final File wktFile = new File("src/test/resources/wkt/github_issue_27_01_multilinestring.wkt"); 32 | final File outputFile = new File("linestring.mvt"); 33 | final GeometryFactory gf = new GeometryFactory(); 34 | final WKTReader reader = new WKTReader(gf); 35 | final MvtLayerParams mvtLayerParams = MvtLayerParams.DEFAULT; 36 | 37 | try(FileReader fileReader = new FileReader(wktFile)) { 38 | final Geometry wktGeom = reader.read(fileReader); 39 | final Envelope env = new Envelope(545014.05D, 545043.15D, 6867178.74D, 6867219.47D); 40 | final TileGeomResult tileGeom = JtsAdapter.createTileGeom(wktGeom, env, gf, mvtLayerParams, g -> true); 41 | 42 | final MvtLayerProps mvtLayerProps = new MvtLayerProps(); 43 | final VectorTile.Tile.Builder tileBuilder = VectorTile.Tile.newBuilder(); 44 | final VectorTile.Tile.Layer.Builder layerBuilder = MvtLayerBuild.newLayerBuilder("myLayerName", mvtLayerParams); 45 | final List features = JtsAdapter.toFeatures(tileGeom.mvtGeoms, mvtLayerProps, new UserDataIgnoreConverter()); 46 | layerBuilder.addAllFeatures(features); 47 | MvtLayerBuild.writeProps(layerBuilder, mvtLayerProps); 48 | tileBuilder.addLayers(layerBuilder); 49 | 50 | final VectorTile.Tile mvt = tileBuilder.build(); 51 | try { 52 | Files.write(outputFile.toPath(), mvt.toByteArray()); 53 | } catch (IOException e) { 54 | LoggerFactory.getLogger(JtsAdapterIssue27Test.class).error(e.getMessage(), e); 55 | } 56 | 57 | // Examine geometry output, will be a bit screwed but observe line segments are present 58 | final JtsMvt jtsMvt = MvtReader.loadMvt(outputFile, gf, new TagIgnoreConverter()); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/encoding/MvtValue.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.encoding; 2 | 3 | import com.wdtinc.mapbox_vector_tile.VectorTile; 4 | 5 | /** 6 | * Utility class for working with {@link VectorTile.Tile.Value} instances. 7 | * 8 | * @see VectorTile.Tile.Value 9 | */ 10 | public final class MvtValue { 11 | 12 | /** 13 | * Covert an {@link Object} to a new {@link VectorTile.Tile.Value} instance. 14 | * 15 | * @param value target for conversion 16 | * @return new instance with String or primitive value set 17 | */ 18 | public static VectorTile.Tile.Value toValue(Object value) { 19 | final VectorTile.Tile.Value.Builder tileValue = VectorTile.Tile.Value.newBuilder(); 20 | 21 | if(value instanceof Boolean) { 22 | tileValue.setBoolValue((Boolean) value); 23 | 24 | } else if(value instanceof Integer) { 25 | tileValue.setSintValue((Integer) value); 26 | 27 | } else if(value instanceof Long) { 28 | tileValue.setSintValue((Long) value); 29 | 30 | } else if(value instanceof Float) { 31 | tileValue.setFloatValue((Float) value); 32 | 33 | } else if(value instanceof Double) { 34 | tileValue.setDoubleValue((Double) value); 35 | 36 | } else if(value instanceof String) { 37 | tileValue.setStringValue((String) value); 38 | } 39 | 40 | return tileValue.build(); 41 | } 42 | 43 | /** 44 | * Convert {@link VectorTile.Tile.Value} to String or boxed primitive object. 45 | * 46 | * @param value target for conversion 47 | * @return String or boxed primitive 48 | */ 49 | public static Object toObject(VectorTile.Tile.Value value) { 50 | Object result = null; 51 | 52 | if(value.hasDoubleValue()) { 53 | result = value.getDoubleValue(); 54 | 55 | } else if(value.hasFloatValue()) { 56 | result = value.getFloatValue(); 57 | 58 | } else if(value.hasIntValue()) { 59 | result = value.getIntValue(); 60 | 61 | } else if(value.hasBoolValue()) { 62 | result = value.getBoolValue(); 63 | 64 | } else if(value.hasStringValue()) { 65 | result = value.getStringValue(); 66 | 67 | } else if(value.hasSintValue()) { 68 | result = value.getSintValue(); 69 | 70 | } else if(value.hasUintValue()) { 71 | result = value.getUintValue(); 72 | } 73 | 74 | return result; 75 | } 76 | 77 | /** 78 | * Check if {@code value} is valid for encoding as a MVT layer property value. 79 | * 80 | * @param value target to check 81 | * @return true is the object is a type that is supported by MVT 82 | */ 83 | public static boolean isValidPropValue(Object value) { 84 | boolean isValid = false; 85 | 86 | if(value instanceof Boolean || value instanceof Integer || value instanceof Long 87 | || value instanceof Float || value instanceof Double || value instanceof String) { 88 | isValid = true; 89 | } 90 | 91 | return isValid; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/adapt/jts/TagKeyValueMapConverter.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.adapt.jts; 2 | 3 | import com.wdtinc.mapbox_vector_tile.encoding.MvtValue; 4 | import com.wdtinc.mapbox_vector_tile.VectorTile; 5 | import com.wdtinc.mapbox_vector_tile.util.JdkUtils; 6 | 7 | import java.util.*; 8 | 9 | /** 10 | * Convert MVT tags list to a {@link Map} of {@link String} to {@link Object}. Tags indices that are out 11 | * of range of the key or value list are ignored. 12 | * 13 | * @see ITagConverter 14 | */ 15 | public final class TagKeyValueMapConverter implements ITagConverter { 16 | 17 | /** 18 | * If true, return null user data when tags are empty. 19 | */ 20 | private final boolean nullIfEmpty; 21 | 22 | /** 23 | * If true, add id to user data object. 24 | */ 25 | private final boolean addId; 26 | 27 | /** 28 | * The {@link Map} key for the feature id. 29 | */ 30 | private final String idKey; 31 | 32 | /** 33 | * Always created user data object, even with empty tags. Ignore feature ids. 34 | */ 35 | public TagKeyValueMapConverter() { 36 | this(false); 37 | } 38 | 39 | /** 40 | * Ignore feature ids. 41 | * 42 | * @param nullIfEmpty if true, return null user data when tags are empty 43 | */ 44 | public TagKeyValueMapConverter(boolean nullIfEmpty) { 45 | this.nullIfEmpty = nullIfEmpty; 46 | this.addId = false; 47 | this.idKey = null; 48 | } 49 | 50 | /** 51 | * Store feature ids using idKey. Id value may be null if not present. 52 | * 53 | * @param nullIfEmpty if true, return null user data when tags are empty 54 | * @param idKey key name to use for feature id value 55 | */ 56 | public TagKeyValueMapConverter(boolean nullIfEmpty, String idKey) { 57 | JdkUtils.requireNonNull(idKey); 58 | 59 | this.nullIfEmpty = nullIfEmpty; 60 | this.addId = true; 61 | this.idKey = idKey; 62 | } 63 | 64 | @Override 65 | public Object toUserData(Long id, List tags, List keysList, 66 | List valuesList) { 67 | 68 | // Guard: empty 69 | if(nullIfEmpty && tags.size() < 1 && (!addId || id == null)) { 70 | return null; 71 | } 72 | 73 | 74 | final Map userData = new LinkedHashMap<>(((tags.size() + 1) / 2)); 75 | 76 | // Add feature properties 77 | int keyIndex; 78 | int valIndex; 79 | boolean valid; 80 | 81 | for(int i = 0; i < tags.size() - 1; i += 2) { 82 | keyIndex = tags.get(i); 83 | valIndex = tags.get(i + 1); 84 | 85 | valid = keyIndex >= 0 && keyIndex < keysList.size() 86 | && valIndex >= 0 && valIndex < valuesList.size(); 87 | 88 | if(valid) { 89 | userData.put(keysList.get(keyIndex), MvtValue.toObject(valuesList.get(valIndex))); 90 | } 91 | } 92 | 93 | // Add ID, value may be null 94 | if(addId) { 95 | userData.put(idKey, id); 96 | } 97 | 98 | return userData; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/resources/vector_tile.proto: -------------------------------------------------------------------------------- 1 | // Protocol Version 1 2 | 3 | syntax = "proto2"; 4 | 5 | package vector_tile; 6 | 7 | option java_package = "com.wdtinc.mapbox_vector_tile"; 8 | option java_outer_classname = "VectorTile"; 9 | 10 | 11 | message Tile { 12 | 13 | // GeomType is described in section 4.3.4 of the specification 14 | enum GeomType { 15 | UNKNOWN = 0; 16 | POINT = 1; 17 | LINESTRING = 2; 18 | POLYGON = 3; 19 | } 20 | 21 | // Variant type encoding 22 | // The use of values is described in section 4.1 of the specification 23 | message Value { 24 | // Exactly one of these values must be present in a valid message 25 | optional string string_value = 1; 26 | optional float float_value = 2; 27 | optional double double_value = 3; 28 | optional int64 int_value = 4; 29 | optional uint64 uint_value = 5; 30 | optional sint64 sint_value = 6; 31 | optional bool bool_value = 7; 32 | 33 | extensions 8 to max; 34 | } 35 | 36 | // Features are described in section 4.2 of the specification 37 | message Feature { 38 | optional uint64 id = 1 [ default = 0 ]; 39 | 40 | // Tags of this feature are encoded as repeated pairs of 41 | // integers. 42 | // A detailed description of tags is located in sections 43 | // 4.2 and 4.4 of the specification 44 | repeated uint32 tags = 2 [ packed = true ]; 45 | 46 | // The type of geometry stored in this feature. 47 | optional GeomType type = 3 [ default = UNKNOWN ]; 48 | 49 | // Contains a stream of commands and parameters (vertices). 50 | // A detailed description on geometry encoding is located in 51 | // section 4.3 of the specification. 52 | repeated uint32 geometry = 4 [ packed = true ]; 53 | } 54 | 55 | // Layers are described in section 4.1 of the specification 56 | message Layer { 57 | // Any compliant implementation must first read the version 58 | // number encoded in this message and choose the correct 59 | // implementation for this version number before proceeding to 60 | // decode other parts of this message. 61 | required uint32 version = 15 [ default = 1 ]; 62 | 63 | required string name = 1; 64 | 65 | // The actual features in this tile. 66 | repeated Feature features = 2; 67 | 68 | // Dictionary encoding for keys 69 | repeated string keys = 3; 70 | 71 | // Dictionary encoding for values 72 | repeated Value values = 4; 73 | 74 | // Although this is an "optional" field it is required by the specification. 75 | // See https://github.com/mapbox/vector-tile-spec/issues/47 76 | optional uint32 extent = 5 [ default = 4096 ]; 77 | 78 | extensions 16 to max; 79 | } 80 | 81 | repeated Layer layers = 3; 82 | 83 | extensions 16 to 8191; 84 | } -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/util/Vec2d.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.util; 2 | 3 | /** 4 | * Mutable Vector with double-valued x and y dimensions. 5 | */ 6 | public final class Vec2d { 7 | 8 | public double x, y; 9 | 10 | /** 11 | * Construct instance with x = 0, y = 0. 12 | */ 13 | public Vec2d() { 14 | set(0d, 0d); 15 | } 16 | 17 | /** 18 | * Construct instance with (x, y) values set to passed parameters. 19 | * 20 | * @param x value in x 21 | * @param y value in y 22 | */ 23 | public Vec2d(double x, double y) { 24 | set(x, y); 25 | } 26 | 27 | /** 28 | * Constructs instance with values from the input vector 'v'. 29 | * 30 | * @param v The vector 31 | */ 32 | public Vec2d(Vec2d v) { 33 | set(v); 34 | } 35 | 36 | /** 37 | * Set the x and y values of this vector. Return this vector for chaining. 38 | * 39 | * @param x value in x 40 | * @param y value in y 41 | * @return this vector for chaining 42 | */ 43 | public Vec2d set(double x, double y) { 44 | this.x = x; 45 | this.y = y; 46 | 47 | return this; 48 | } 49 | 50 | /** 51 | * Set the x and y values of this vector to match input vector 'v'. Return this vector for chaining. 52 | * 53 | * @param v contains values to copy 54 | * @return this vector for chaining 55 | */ 56 | public Vec2d set(Vec2d v) { 57 | return set(v.x, v.y); 58 | } 59 | 60 | /** 61 | * Adds the given values to this vector. Return this vector for chaining. 62 | * 63 | * @param x value in x 64 | * @param y value in y 65 | * @return this vector for chaining 66 | */ 67 | public Vec2d add(double x, double y) { 68 | this.x += x; 69 | this.y += y; 70 | 71 | return this; 72 | } 73 | 74 | /** 75 | * Adds the given vector 'v' to this vector. Return this vector for chaining. 76 | * 77 | * @param v vector to add 78 | * @return this vector for chaining 79 | */ 80 | public Vec2d add(Vec2d v) { 81 | return add(v.x, v.y); 82 | } 83 | 84 | /** 85 | * Subtracts the given values from this vector. Return this vector for chaining. 86 | * 87 | * @param x value in x to subtract 88 | * @param y value in y to subtract 89 | * @return this vector for chaining 90 | */ 91 | public Vec2d sub(double x, double y) { 92 | this.x -= x; 93 | this.y -= y; 94 | 95 | return this; 96 | } 97 | 98 | /** 99 | * Subtracts the given vector 'v' from this vector. Return this vector for chaining. 100 | * 101 | * @param v vector to subtract 102 | * @return this vector for chaining 103 | */ 104 | public Vec2d sub(Vec2d v) { 105 | return sub(v.x, v.y); 106 | } 107 | 108 | /** 109 | * Scales this vector's values by a constant. 110 | * 111 | * @param scalar constant to scale this vector's values by 112 | * @return this vector for chaining 113 | */ 114 | public Vec2d scale(double scalar) { 115 | this.x *= scalar; 116 | this.y *= scalar; 117 | 118 | return this; 119 | } 120 | 121 | @Override 122 | public String toString() { 123 | return "(" + x + "," + y + ")"; 124 | } 125 | } -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/adapt/jts/UserDataKeyValueMapConverter.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.adapt.jts; 2 | 3 | import com.wdtinc.mapbox_vector_tile.VectorTile; 4 | import com.wdtinc.mapbox_vector_tile.build.MvtLayerProps; 5 | import com.wdtinc.mapbox_vector_tile.util.JdkUtils; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.util.Map; 9 | 10 | /** 11 | * Convert simple user data {@link Map} where the keys are {@link String} and values are {@link Object}. Supports 12 | * converting a specific map key to a user id. If the key to user id conversion fails, the error occurs silently 13 | * and the id is discarded. 14 | * 15 | * @see IUserDataConverter 16 | */ 17 | public final class UserDataKeyValueMapConverter implements IUserDataConverter { 18 | 19 | /** 20 | * If true, set feature id from user data. 21 | */ 22 | private final boolean setId; 23 | 24 | /** 25 | * The {@link Map} key for the feature id. 26 | */ 27 | private final String idKey; 28 | 29 | /** 30 | * Does not set feature id. 31 | */ 32 | public UserDataKeyValueMapConverter() { 33 | this.setId = false; 34 | this.idKey = null; 35 | } 36 | 37 | /** 38 | * Tries to set feature id using provided user data {@link Map} key. 39 | * 40 | * @param idKey user data {@link Map} key for getting id value. 41 | */ 42 | public UserDataKeyValueMapConverter(String idKey) { 43 | JdkUtils.requireNonNull(idKey); 44 | this.setId = true; 45 | this.idKey = idKey; 46 | } 47 | 48 | @Override 49 | public void addTags(Object userData, MvtLayerProps layerProps, VectorTile.Tile.Feature.Builder featureBuilder) { 50 | if(userData != null) { 51 | try { 52 | @SuppressWarnings("unchecked") 53 | final Map userDataMap = (Map)userData; 54 | 55 | for (Map.Entry e : userDataMap.entrySet()) { 56 | final String key = e.getKey(); 57 | final Object value = e.getValue(); 58 | 59 | if(key != null && value != null) { 60 | final int valueIndex = layerProps.addValue(value); 61 | 62 | if(valueIndex >= 0) { 63 | featureBuilder.addTags(layerProps.addKey(key)); 64 | featureBuilder.addTags(valueIndex); 65 | } 66 | } 67 | } 68 | 69 | // Set feature id value 70 | if(setId) { 71 | final Object idValue = userDataMap.get(idKey); 72 | if (idValue != null) { 73 | if(idValue instanceof Long || idValue instanceof Integer 74 | || idValue instanceof Float || idValue instanceof Double 75 | || idValue instanceof Byte || idValue instanceof Short) { 76 | featureBuilder.setId((long)idValue); 77 | } else if(idValue instanceof String) { 78 | try { 79 | featureBuilder.setId(Long.parseLong((String) idValue)); 80 | } catch (NumberFormatException expected) { } 81 | } 82 | } 83 | } 84 | 85 | } catch (ClassCastException e) { 86 | LoggerFactory.getLogger(UserDataKeyValueMapConverter.class).error(e.getMessage(), 87 | e); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/test/java/com/wdtinc/mapbox_vector_tile/adapt/jts/MvtEncoderTest.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.adapt.jts; 2 | 3 | import org.locationtech.jts.geom.Coordinate; 4 | import org.locationtech.jts.geom.Geometry; 5 | import org.locationtech.jts.geom.GeometryFactory; 6 | import org.locationtech.jts.geom.Point; 7 | import com.wdtinc.mapbox_vector_tile.adapt.jts.model.JtsLayer; 8 | import com.wdtinc.mapbox_vector_tile.adapt.jts.model.JtsMvt; 9 | import org.junit.Test; 10 | 11 | import java.io.ByteArrayInputStream; 12 | import java.io.IOException; 13 | import java.util.*; 14 | 15 | import static java.util.Arrays.asList; 16 | import static java.util.Collections.singletonList; 17 | import static org.junit.Assert.assertEquals; 18 | 19 | public final class MvtEncoderTest { 20 | 21 | private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory(); 22 | 23 | private static JtsMvt decode(byte[] bytes) throws IOException { 24 | return MvtReader.loadMvt(new ByteArrayInputStream(bytes), GEOMETRY_FACTORY, 25 | new TagKeyValueMapConverter()); 26 | } 27 | 28 | @Test 29 | public void singleLayer() throws IOException { 30 | Collection geometries = PointGen.australia(); 31 | 32 | JtsLayer layer = new JtsLayer("animals", geometries); 33 | JtsMvt mvt = new JtsMvt(singletonList(layer)); 34 | 35 | final byte[] encoded = MvtEncoder.encode(mvt); 36 | assertEquals(mvt, decode(encoded)); 37 | } 38 | 39 | @Test 40 | public void multipleLayers() throws IOException { 41 | JtsLayer layer = new JtsLayer("Australia", PointGen.australia()); 42 | JtsLayer layer2 = new JtsLayer("United Kingdom", PointGen.uk()); 43 | JtsLayer layer3 = new JtsLayer("United States of America", PointGen.usa()); 44 | JtsMvt mvt = new JtsMvt(asList(layer, layer2, layer3)); 45 | 46 | final byte[] encoded = MvtEncoder.encode(mvt); 47 | assertEquals(mvt, decode(encoded)); 48 | } 49 | 50 | private static class PointGen { 51 | 52 | /** 53 | * Generate Geometries with this default specification. 54 | */ 55 | private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory(); 56 | private static final Random RANDOM = new Random(); 57 | 58 | private static Collection australia() { 59 | return getPoints( 60 | createPoint("Koala"), 61 | createPoint("Wombat"), 62 | createPoint("Platypus"), 63 | createPoint("Dingo"), 64 | createPoint("Croc")); 65 | } 66 | 67 | private static Collection uk() { 68 | return getPoints( 69 | createPoint("Hare"), 70 | createPoint("Frog"), 71 | createPoint("Robin"), 72 | createPoint("Fox"), 73 | createPoint("Hedgehog"), 74 | createPoint("Bulldog")); 75 | } 76 | 77 | private static Collection usa() { 78 | return getPoints( 79 | createPoint("Cougar"), 80 | createPoint("Raccoon"), 81 | createPoint("Beaver"), 82 | createPoint("Wolf"), 83 | createPoint("Bear"), 84 | createPoint("Coyote")); 85 | } 86 | 87 | private static Collection getPoints(Point... points) { 88 | return asList(points); 89 | } 90 | 91 | private static Point createPoint(String name) { 92 | Coordinate coord = new Coordinate(RANDOM.nextInt(4096), RANDOM.nextInt(4096)); 93 | Point point = GEOMETRY_FACTORY.createPoint(coord); 94 | 95 | Map attributes = new LinkedHashMap<>(); 96 | attributes.put("id", name.hashCode()); 97 | attributes.put("name", name); 98 | point.setUserData(attributes); 99 | 100 | return point; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/test/java/com/wdtinc/mapbox_vector_tile/adapt/jts/model/JtsLayerTest.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.adapt.jts.model; 2 | 3 | import org.locationtech.jts.geom.Coordinate; 4 | import org.locationtech.jts.geom.Geometry; 5 | import org.locationtech.jts.geom.GeometryFactory; 6 | import org.locationtech.jts.geom.Point; 7 | import org.junit.Test; 8 | 9 | import java.util.*; 10 | 11 | import static org.junit.Assert.assertEquals; 12 | import static org.junit.Assert.assertFalse; 13 | import static org.junit.Assert.assertTrue; 14 | 15 | public final class JtsLayerTest { 16 | 17 | private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory(); 18 | 19 | @Test 20 | public void testLayerExtent() { 21 | String layerName = "Points of Interest"; 22 | JtsLayer layer = new JtsLayer(layerName, new ArrayList<>(), 2048); 23 | 24 | int actual = 2048; 25 | int expected = layer.getExtent(); 26 | assertEquals(expected,actual); 27 | } 28 | 29 | @Test 30 | public void testLayerName() { 31 | String layerName = "Points of Interest"; 32 | JtsLayer layer = new JtsLayer(layerName); 33 | 34 | String actual = layer.getName(); 35 | String expected = layerName; 36 | assertEquals(expected, actual); 37 | } 38 | 39 | @Test 40 | public void testLayerCollection() { 41 | String layerName = "Points of Interest"; 42 | List geometries = new ArrayList<>(); 43 | 44 | JtsLayer layer = new JtsLayer(layerName, geometries); 45 | 46 | String actualName = layer.getName(); 47 | String expectedName = layerName; 48 | assertEquals(expectedName, actualName); 49 | 50 | Collection actualGeometry = layer.getGeometries(); 51 | Collection expectedGeometry = geometries; 52 | assertEquals(expectedGeometry, actualGeometry); 53 | } 54 | 55 | @Test 56 | public void testAddGeometry() { 57 | String layerName = "Points of Interest"; 58 | List geometries = new ArrayList<>(); 59 | 60 | Point point = createPoint(new int[]{51, 0}); 61 | 62 | JtsLayer layer = new JtsLayer(layerName, geometries); 63 | layer.getGeometries().add(point); 64 | 65 | assertTrue(layer.getGeometries().contains(point)); 66 | } 67 | 68 | 69 | @Test 70 | public void testAddGeometries() { 71 | String layerName = "Points of Interest"; 72 | List geometries = new ArrayList<>(); 73 | 74 | Point point = createPoint(new int[]{50, 0}); 75 | Point point2 = createPoint(new int[]{51, 1}); 76 | Collection points = Arrays.asList(point, point2); 77 | 78 | JtsLayer layer = new JtsLayer(layerName, geometries); 79 | layer.getGeometries().addAll(points); 80 | 81 | assertTrue(layer.getGeometries().containsAll(Arrays.asList(point, point2))); 82 | } 83 | 84 | @Test 85 | public void testEquality() { 86 | JtsLayer layer1 = new JtsLayer("apples"); 87 | JtsLayer layer1Duplicate = new JtsLayer("apples"); 88 | assertTrue(layer1.equals(layer1Duplicate)); 89 | 90 | JtsLayer layer2 = new JtsLayer("oranges"); 91 | assertFalse(layer1.equals(layer2)); 92 | } 93 | 94 | @Test 95 | public void testToString() { 96 | JtsLayer layer1 = new JtsLayer("apples"); 97 | String actual = layer1.toString(); 98 | String expected = "Layer{name='apples', geometries=[], extent=4096}"; 99 | assertEquals(expected, actual); 100 | } 101 | 102 | @Test 103 | public void testHash() { 104 | JtsLayer layer = new JtsLayer("code"); 105 | int actual = layer.hashCode(); 106 | int expected = -1354967378; 107 | assertEquals(expected, actual); 108 | } 109 | 110 | @Test(expected = IllegalArgumentException.class) 111 | public void testNullName() { 112 | new JtsLayer(null); 113 | } 114 | 115 | @Test(expected = IllegalArgumentException.class) 116 | public void testNullCollection() { 117 | new JtsLayer("apples", null); 118 | } 119 | 120 | private Point createPoint(int[] coordinates) { 121 | return GEOMETRY_FACTORY.createPoint(new Coordinate(coordinates[0], coordinates[1])); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/adapt/jts/model/JtsLayer.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.adapt.jts.model; 2 | 3 | import org.locationtech.jts.geom.Geometry; 4 | 5 | import com.wdtinc.mapbox_vector_tile.build.MvtLayerParams; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Collection; 9 | 10 | /** 11 | *

JTS model of a Mapbox Vector Tile (MVT) layer.

12 | * 13 | *

A layer contains a subset of all geographic geometries in the tile.

14 | */ 15 | public class JtsLayer { 16 | 17 | private final String name; 18 | private final Collection geometries; 19 | private final int extent; 20 | 21 | /** 22 | * Create an empty JTS layer. 23 | * 24 | * @param name layer name 25 | * @throws IllegalArgumentException when {@code name} is null 26 | */ 27 | public JtsLayer(String name) { 28 | this(name, new ArrayList<>(0), MvtLayerParams.DEFAULT.extent); 29 | } 30 | 31 | /** 32 | * Create a JTS layer with geometries. 33 | * 34 | * @param name layer name 35 | * @param geometries 36 | * @throws IllegalArgumentException when {@code name} or {@code geometries} are null 37 | */ 38 | public JtsLayer(String name, Collection geometries) { 39 | this(name, geometries, MvtLayerParams.DEFAULT.extent); 40 | } 41 | 42 | /** 43 | * Create a JTS layer with geometries. 44 | * 45 | * @param name layer name 46 | * @param geometries 47 | * @param extent 48 | * @throws IllegalArgumentException when {@code name} or {@code geometries} are null 49 | * or {@code extent} is less than or equal to 0 50 | */ 51 | public JtsLayer(String name, Collection geometries, int extent) { 52 | validate(name, geometries, extent); 53 | this.name = name; 54 | this.geometries = geometries; 55 | this.extent = extent; 56 | } 57 | 58 | /** 59 | * Get a read-only collection of geometry. 60 | * 61 | * @return unmodifiable collection of geometry. 62 | */ 63 | public Collection getGeometries() { 64 | return geometries; 65 | } 66 | 67 | /** 68 | * Get the layer name. 69 | * 70 | * @return name of the layer 71 | */ 72 | public String getName() { 73 | return name; 74 | } 75 | 76 | /** 77 | * Get the layer extent. 78 | * 79 | * @return extent of the layer 80 | */ 81 | public int getExtent() { 82 | return extent; 83 | } 84 | 85 | @Override 86 | public boolean equals(Object o) { 87 | if (this == o) return true; 88 | if (o == null || getClass() != o.getClass()) return false; 89 | 90 | JtsLayer layer = (JtsLayer) o; 91 | 92 | if (extent != layer.getExtent()) return false; 93 | if (name != null ? !name.equals(layer.name) : layer.name != null) return false; 94 | return geometries != null ? geometries.equals(layer.geometries) : layer.geometries == null; 95 | } 96 | 97 | @Override 98 | public int hashCode() { 99 | int result = name != null ? name.hashCode() : 0; 100 | result = 31 * result + extent; 101 | result = 31 * result + (geometries != null ? geometries.hashCode() : 0); 102 | return result; 103 | } 104 | 105 | @Override 106 | public String toString() { 107 | return "Layer{" + 108 | "name='" + name + '\'' + 109 | ", geometries=" + geometries + 110 | ", extent=" + extent+ 111 | '}'; 112 | } 113 | 114 | /** 115 | * Validate the JtsLayer. 116 | * 117 | * @param name mvt layer name 118 | * @param geometries geometries in the tile 119 | * @throws IllegalArgumentException when {@code name} or {@code geometries} are null 120 | */ 121 | private static void validate(String name, Collection geometries, int extent) { 122 | if (name == null) { 123 | throw new IllegalArgumentException("layer name is null"); 124 | } 125 | if (geometries == null) { 126 | throw new IllegalArgumentException("geometry collection is null"); 127 | } 128 | if (extent <= 0) { 129 | throw new IllegalArgumentException("extent is less than or equal to 0"); 130 | } 131 | } 132 | } 133 | 134 | -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/util/JtsGeomStats.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.util; 2 | 3 | import org.locationtech.jts.geom.*; 4 | import com.wdtinc.mapbox_vector_tile.VectorTile; 5 | import com.wdtinc.mapbox_vector_tile.adapt.jts.JtsAdapter; 6 | 7 | import java.util.*; 8 | 9 | public final class JtsGeomStats { 10 | 11 | public static final class FeatureStats { 12 | public int totalPts; 13 | public int repeatedPts; 14 | 15 | @Override 16 | public String toString() { 17 | return "FeatureStats{" + 18 | "totalPts=" + totalPts + 19 | ", repeatedPts=" + repeatedPts + 20 | '}'; 21 | } 22 | } 23 | 24 | public Map featureCounts; 25 | public List featureStats; 26 | 27 | private JtsGeomStats() { 28 | final VectorTile.Tile.GeomType[] geomTypes = VectorTile.Tile.GeomType.values(); 29 | featureCounts = new HashMap<>(geomTypes.length); 30 | 31 | for(VectorTile.Tile.GeomType nextGeomType : geomTypes) { 32 | featureCounts.put(nextGeomType, 0); 33 | } 34 | 35 | this.featureStats = new ArrayList<>(); 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | return "JtsGeomStats{" + 41 | "featureCounts=" + featureCounts + 42 | ", featureStats=" + featureStats + 43 | '}'; 44 | } 45 | 46 | /** 47 | * Get feature counts and feature statistics (points and repeated points). 48 | * 49 | * @param flatGeomList geometry under analysis 50 | * @return the resulting statistics 51 | */ 52 | public static JtsGeomStats getStats(List flatGeomList) { 53 | final JtsGeomStats stats = new JtsGeomStats(); 54 | 55 | for(Geometry nextGeom : flatGeomList) { 56 | final VectorTile.Tile.GeomType geomType = JtsAdapter.toGeomType(nextGeom); 57 | 58 | // Count features by type 59 | Integer value = stats.featureCounts.get(geomType); 60 | value = value == null ? 1 : value + 1; 61 | stats.featureCounts.put(geomType, value); 62 | 63 | // Get stats per feature 64 | stats.featureStats.add(getStats(nextGeom, geomType)); 65 | } 66 | 67 | return stats; 68 | } 69 | 70 | private static FeatureStats getStats(Geometry geom, VectorTile.Tile.GeomType type) { 71 | FeatureStats featureStats; 72 | 73 | switch (type) { 74 | case POINT: 75 | featureStats = pointStats(geom); 76 | break; 77 | case LINESTRING: 78 | featureStats = lineStats(geom); 79 | break; 80 | case POLYGON: 81 | featureStats = polyStats(geom); 82 | break; 83 | default: 84 | featureStats = new FeatureStats(); 85 | } 86 | 87 | return featureStats; 88 | } 89 | 90 | private static FeatureStats pointStats(Geometry geom) { 91 | final FeatureStats featureStats = new FeatureStats(); 92 | 93 | final HashSet pointSet = new HashSet<>(geom.getNumPoints()); 94 | featureStats.totalPts = geom.getNumPoints(); 95 | 96 | for(int i = 0; i < geom.getNumGeometries(); ++i) { 97 | final Point p = (Point) geom.getGeometryN(i); 98 | featureStats.repeatedPts += pointSet.add(p) ? 0 : 1; 99 | } 100 | 101 | return featureStats; 102 | } 103 | 104 | private static FeatureStats lineStats(Geometry geom) { 105 | final FeatureStats featureStats = new FeatureStats(); 106 | 107 | for(int i = 0; i < geom.getNumGeometries(); ++i) { 108 | final LineString lineString = (LineString) geom.getGeometryN(i); 109 | featureStats.totalPts += lineString.getNumPoints(); 110 | featureStats.repeatedPts += checkRepeatedPoints2d(lineString); 111 | } 112 | 113 | return featureStats; 114 | } 115 | 116 | private static FeatureStats polyStats(Geometry geom) { 117 | final FeatureStats featureStats = new FeatureStats(); 118 | 119 | for(int i = 0; i < geom.getNumGeometries(); ++i) { 120 | final Polygon nextPoly = (Polygon) geom.getGeometryN(i); 121 | 122 | // Stats: exterior ring 123 | final LineString exteriorRing = nextPoly.getExteriorRing(); 124 | featureStats.totalPts += exteriorRing.getNumPoints(); 125 | featureStats.repeatedPts += checkRepeatedPoints2d(exteriorRing); 126 | 127 | // Stats: interior rings 128 | for(int ringIndex = 0; ringIndex < nextPoly.getNumInteriorRing(); ++ringIndex) { 129 | 130 | final LineString nextInteriorRing = nextPoly.getInteriorRingN(ringIndex); 131 | featureStats.totalPts += nextInteriorRing.getNumPoints(); 132 | featureStats.repeatedPts += checkRepeatedPoints2d(nextInteriorRing); 133 | } 134 | } 135 | 136 | return featureStats; 137 | } 138 | 139 | private static int checkRepeatedPoints2d(LineString lineString) { 140 | int repeatedPoints = 0; 141 | 142 | final CoordinateSequence coordSeq = lineString.getCoordinateSequence(); 143 | Coordinate nextCoord = null, prevCoord; 144 | for(int i = 0; i < coordSeq.size(); ++i) { 145 | prevCoord = nextCoord; 146 | nextCoord = coordSeq.getCoordinate(i); 147 | if(nextCoord.equals(prevCoord)) { 148 | ++repeatedPoints; 149 | } 150 | } 151 | 152 | return repeatedPoints; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.wdtinc 8 | mapbox-vector-tile 9 | 3.1.1 10 | jar 11 | 12 | Mapbox Vector Tiles - Java 13 | Mapbox Vector Tile Support for Java. 14 | https://github.com/wdtinc/mapbox-vector-tile-java 15 | 16 | 17 | 18 | Apache Licence 2.0 19 | http://www.apache.org/licenses/LICENSE-2.0 20 | repo 21 | 22 | 23 | 24 | 25 | 26 | nhunter 27 | Weather Decision Technologies, Inc. 28 | http://wdtinc.com/ 29 | Nicholas Hunter 30 | https://github.com/ShibaBandit 31 | 32 | 33 | 34 | 35 | scm:git:git://github.com/wdtinc/mapbox-vector-tile-java.git 36 | scm:git:ssh://github.com:wdtinc/mapbox-vector-tile-java.git 37 | https://github.com/wdtinc/mapbox-vector-tile-java 38 | 39 | 40 | 41 | UTF-8 42 | 43 | 44 | 45 | 46 | central 47 | Maven Central 48 | http://repo1.maven.org/maven2/ 49 | 50 | true 51 | warn 52 | 53 | 54 | 55 | 56 | 57 | 58 | ossrh 59 | https://oss.sonatype.org/content/repositories/snapshots 60 | 61 | 62 | ossrh 63 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 64 | 65 | 66 | 67 | 68 | 69 | 70 | org.apache.maven.plugins 71 | maven-compiler-plugin 72 | 3.2 73 | 74 | 1.8 75 | 1.8 76 | 77 | 78 | 79 | org.apache.maven.plugins 80 | maven-surefire-plugin 81 | 2.19.1 82 | 83 | 84 | org.apache.maven.plugins 85 | maven-source-plugin 86 | 2.4 87 | 88 | 89 | attach-sources 90 | 91 | jar 92 | 93 | 94 | 95 | 96 | 97 | org.apache.maven.plugins 98 | maven-javadoc-plugin 99 | 2.10.3 100 | 101 | true 102 | public 103 | -Xdoclint:none 104 | 105 | 106 | 107 | attach-javadocs 108 | 109 | jar 110 | 111 | 112 | 113 | 114 | 115 | org.apache.maven.plugins 116 | maven-gpg-plugin 117 | 1.5 118 | 119 | 120 | sign-artifacts 121 | verify 122 | 123 | sign 124 | 125 | 126 | 127 | 128 | 129 | org.sonatype.plugins 130 | nexus-staging-maven-plugin 131 | 1.6.7 132 | true 133 | 134 | ossrh 135 | https://oss.sonatype.org/ 136 | true 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | org.slf4j 147 | slf4j-api 148 | 1.7.25 149 | 150 | 151 | 152 | 153 | com.google.protobuf 154 | protobuf-java 155 | 3.5.1 156 | 157 | 158 | 159 | 160 | org.locationtech.jts 161 | jts-core 162 | 1.15.1 163 | 164 | 165 | 166 | 167 | junit 168 | junit 169 | 4.12 170 | test 171 | 172 | 173 | org.slf4j 174 | slf4j-simple 175 | 1.7.25 176 | test 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /src/test/java/com/wdtinc/mapbox_vector_tile/adapt/jts/MvtReaderTest.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.adapt.jts; 2 | 3 | import org.locationtech.jts.geom.Geometry; 4 | import org.locationtech.jts.geom.GeometryFactory; 5 | import org.locationtech.jts.geom.LineString; 6 | import org.locationtech.jts.geom.MultiPolygon; 7 | import org.locationtech.jts.geom.Polygon; 8 | import org.locationtech.jts.geom.PrecisionModel; 9 | import org.locationtech.jts.geom.impl.PackedCoordinateSequenceFactory; 10 | 11 | import com.wdtinc.mapbox_vector_tile.adapt.jts.model.JtsLayer; 12 | import com.wdtinc.mapbox_vector_tile.adapt.jts.model.JtsMvt; 13 | import com.wdtinc.mapbox_vector_tile.util.JtsGeomStats; 14 | import org.junit.Test; 15 | import org.slf4j.LoggerFactory; 16 | 17 | import java.io.File; 18 | import java.io.IOException; 19 | import java.util.*; 20 | 21 | import static org.junit.Assert.assertEquals; 22 | import static org.junit.Assert.assertTrue; 23 | import static org.junit.Assert.fail; 24 | 25 | /** 26 | * Test reading MVTs. 27 | */ 28 | public final class MvtReaderTest { 29 | 30 | private static final double DOUBLE_DELTA = 1e-10; 31 | 32 | private static final int NUMBER_OF_DIMENSIONS = 2; 33 | private static final int SRID = 0; 34 | 35 | @Test 36 | public void testLayers() { 37 | try { 38 | JtsMvt result = MvtReader.loadMvt( 39 | new File("src/test/resources/vec_tile_test/game.mvt"), 40 | createGeometryFactory(), 41 | new TagKeyValueMapConverter()); 42 | 43 | final Collection layerValues = result.getLayers(); 44 | final int actualCount = layerValues.size(); 45 | final int expectedCount = 4; 46 | assertEquals(expectedCount, actualCount); 47 | 48 | assertTrue(result.getLayer("health") != null); 49 | assertTrue(result.getLayer("bombs") != null); 50 | assertTrue(result.getLayer("enemies") != null); 51 | assertTrue(result.getLayer("bullet") != null); 52 | 53 | // verify order 54 | final Iterator layerIterator = layerValues.iterator(); 55 | assertTrue(layerIterator.next() == result.getLayer("bombs")); 56 | assertTrue(layerIterator.next() == result.getLayer("health")); 57 | assertTrue(layerIterator.next() == result.getLayer("enemies")); 58 | assertTrue(layerIterator.next() == result.getLayer("bullet")); 59 | } catch (IOException e) { 60 | fail(e.getMessage()); 61 | } 62 | } 63 | 64 | @Test 65 | public void simpleTest() { 66 | try { 67 | // Load multipolygon z0 tile 68 | final JtsMvt mvt = loadMvt("src/test/resources/vec_tile_test/0/0/0.mvt"); 69 | 70 | List geoms = getAllGeometries(mvt); 71 | 72 | // Debug stats of multipolygon 73 | final JtsGeomStats stats = JtsGeomStats.getStats(geoms); 74 | LoggerFactory.getLogger(MvtReaderTest.class).info("Stats: {}", stats); 75 | } catch (IOException e) { 76 | fail(e.getMessage()); 77 | } 78 | } 79 | 80 | @Test 81 | public void testNegExtPolyRings() { 82 | try { 83 | // Single MultiPolygon with two triangles that have negative area from shoelace formula 84 | // Support for 'V1' MVTs. 85 | final JtsMvt mvt = loadMvt( 86 | "src/test/resources/mapbox/vector_tile_js/multi_poly_neg_exters.mvt", 87 | MvtReader.RING_CLASSIFIER_V1); 88 | final List geoms = getAllGeometries(mvt); 89 | 90 | assertEquals(1, geoms.size()); 91 | assertTrue(geoms.get(0) instanceof MultiPolygon); 92 | final MultiPolygon multiPolygon = (MultiPolygon) geoms.get(0); 93 | assertEquals(2, multiPolygon.getNumGeometries()); 94 | { 95 | final Polygon polygon = (Polygon) multiPolygon.getGeometryN(0); 96 | assertEquals(0, polygon.getNumInteriorRing()); 97 | final LineString exteriorRing = polygon.getExteriorRing(); 98 | assertEquals(4, exteriorRing.getNumPoints()); 99 | assertEquals(2059.0, exteriorRing.getCoordinateN(0).x, DOUBLE_DELTA); 100 | assertEquals(2048.0, exteriorRing.getCoordinateN(0).y, DOUBLE_DELTA); 101 | assertEquals(2048.0, exteriorRing.getCoordinateN(1).x, DOUBLE_DELTA); 102 | assertEquals(2048.0, exteriorRing.getCoordinateN(1).y, DOUBLE_DELTA); 103 | assertEquals(2059.0, exteriorRing.getCoordinateN(2).x, DOUBLE_DELTA); 104 | assertEquals(2037.0, exteriorRing.getCoordinateN(2).y, DOUBLE_DELTA); 105 | assertEquals(2059.0, exteriorRing.getCoordinateN(3).x, DOUBLE_DELTA); 106 | assertEquals(2048.0, exteriorRing.getCoordinateN(3).y, DOUBLE_DELTA); 107 | } 108 | { 109 | final Polygon polygon = (Polygon) multiPolygon.getGeometryN(1); 110 | assertEquals(0, polygon.getNumInteriorRing()); 111 | final LineString exteriorRing = polygon.getExteriorRing(); 112 | assertEquals(4, exteriorRing.getNumPoints()); 113 | assertEquals(2037.0, exteriorRing.getCoordinateN(0).x, DOUBLE_DELTA); 114 | assertEquals(2059.0, exteriorRing.getCoordinateN(0).y, DOUBLE_DELTA); 115 | assertEquals(2037.0, exteriorRing.getCoordinateN(1).x, DOUBLE_DELTA); 116 | assertEquals(2048.0, exteriorRing.getCoordinateN(1).y, DOUBLE_DELTA); 117 | assertEquals(2048.0, exteriorRing.getCoordinateN(2).x, DOUBLE_DELTA); 118 | assertEquals(2048.0, exteriorRing.getCoordinateN(2).y, DOUBLE_DELTA); 119 | assertEquals(2037.0, exteriorRing.getCoordinateN(3).x, DOUBLE_DELTA); 120 | assertEquals(2059.0, exteriorRing.getCoordinateN(3).y, DOUBLE_DELTA); 121 | } 122 | } catch (IOException e) { 123 | fail(e.getMessage()); 124 | } 125 | } 126 | 127 | private List getAllGeometries(JtsMvt mvt) { 128 | List allGeoms = new ArrayList<>(); 129 | for (JtsLayer l : mvt.getLayers()) { 130 | allGeoms.addAll(l.getGeometries()); 131 | } 132 | return allGeoms; 133 | } 134 | 135 | private static JtsMvt loadMvt(String file) throws IOException { 136 | return MvtReader.loadMvt( 137 | new File(file), 138 | createGeometryFactory(), 139 | new TagKeyValueMapConverter()); 140 | } 141 | 142 | private static JtsMvt loadMvt(String file, 143 | MvtReader.RingClassifier ringClassifier) throws IOException { 144 | return MvtReader.loadMvt( 145 | new File(file), 146 | createGeometryFactory(), 147 | new TagKeyValueMapConverter(), 148 | ringClassifier); 149 | } 150 | 151 | private static GeometryFactory createGeometryFactory() { 152 | final PrecisionModel precisionModel = new PrecisionModel(); 153 | final PackedCoordinateSequenceFactory coordinateSequenceFactory = 154 | new PackedCoordinateSequenceFactory(PackedCoordinateSequenceFactory.DOUBLE, NUMBER_OF_DIMENSIONS); 155 | return new GeometryFactory(precisionModel, SRID, coordinateSequenceFactory); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # MapBox Vector Tile - Java 3 | 4 | [![Build Status](https://travis-ci.org/wdtinc/mapbox-vector-tile-java.svg?branch=master)](https://travis-ci.org/wdtinc/mapbox-vector-tile-java) 5 | 6 | Contents 7 | 8 | - [Overview](#overview) 9 | - [Dependency](#dependency) 10 | - [Reading MVTs](#reading-mvts) 11 | - [Building and Writing MVTs](#building-and-writing-mvts) 12 | - [Buffering Polygons Beyond MVT Extent](#buffering-polygons-beyond-mvt-extent) 13 | - [Examples](#examples) 14 | - [Generate VectorTile class using .proto](#how-to-generate-vectortile-class-using-vector_tile.proto) 15 | - [Issues](#issues) 16 | - [Contributing](#contributing) 17 | - [License](#license) 18 | 19 | ## Overview 20 | 21 | Provides: 22 | 23 | * protoc generated model for Mapbox Vector Tiles v2.1. 24 | * Provides MVT encoding through use of the Java Topology Suite (JTS). 25 | * Android API level 15 compatibility (as of version 3.0.0). 26 | 27 | See: 28 | 29 | * https://www.mapbox.com/vector-tiles/specification/ for overview of the MVT spec. 30 | * https://github.com/mapbox/vector-tile-spec/tree/master/2.1 for details on the MVT spec. 31 | * https://developers.google.com/protocol-buffers/ for details on protoc. 32 | * https://projects.eclipse.org/projects/locationtech.jts for details on JTS. 33 | 34 | ### Dependency 35 | 36 | #### Maven 37 | 38 | Latest version using JTS 15 with android API level 15 support: 39 | 40 | ```xml 41 | 42 | com.wdtinc 43 | mapbox-vector-tile 44 | 3.1.1 45 | 46 | ``` 47 | 48 | JTS 14 with no android support: 49 | 50 | ```xml 51 | 52 | com.wdtinc 53 | mapbox-vector-tile 54 | 2.0.0 55 | 56 | ``` 57 | 58 | #### Gradle 59 | 60 | Latest version using JTS 15 with android API level 15 support: 61 | 62 | ``` 63 | compile 'com.wdtinc:mapbox-vector-tile:3.1.1' 64 | ``` 65 | 66 | JTS 14 with no android support: 67 | 68 | ``` 69 | compile 'com.wdtinc:mapbox-vector-tile:2.0.0' 70 | ``` 71 | 72 | ### Reading MVTs 73 | 74 | Per-tile geometry conversion overview: 75 | 76 | ![Image of Geometry Conversion Overview](docs/mvt_read_flow.png) 77 | 78 | Use MvtReader.loadMvt() to load MVT data from a path or input stream 79 | into JTS geometry. The TagKeyValueMapConverter instance will convert 80 | MVT feature tags to a Map with primitive values. The map will be 81 | stored as a JTS geometry user data object within the Geometry. 82 | 83 | The JtsMvt object wraps the JTS Geometry with MVT layer information 84 | and structure. 85 | 86 | ```java 87 | GeometryFactory geomFactory = new GeometryFactory(); 88 | 89 | JtsMvt jtsMvt = MvtReader.loadMvt( 90 | Paths.get("path/to/your.mvt"), 91 | geomFactory, 92 | new TagKeyValueMapConverter()); 93 | 94 | 95 | // Allow negative-area exterior rings with classifier 96 | // (recommended for Mapbox compatibility) 97 | JtsMvt jtsMvt = MvtReader.loadMvt( 98 | Paths.get("path/to/your.mvt"), 99 | geomFactory, 100 | new TagKeyValueMapConverter(), 101 | MvtReader.RING_CLASSIFIER_V1); 102 | ``` 103 | 104 | ### Building and Writing MVTs 105 | 106 | Per-layer geometry conversion overview: 107 | 108 | ![Image of Geometry Conversion Overview](docs/mvt_build_flow.png) 109 | 110 | #### 1) Create or Load JTS Geometry 111 | 112 | Create or load any JTS Geometry that will be included in the MVT. The Geometries are assumed 113 | to be in the global/world units for your target projection. Example: meters for EPSG:3857. 114 | 115 | #### 2) Create Tiled JTS Geometry in MVT Coordinates 116 | 117 | Create tiled JTS geometry with JtsAdapter#createTileGeom(). MVTs currently 118 | do not support feature collections so any JTS geometry collections will be flattened 119 | to a single level. A TileGeomResult will contain the world/global intersection 120 | geometry from clipping as well as the actual MVT geometry that uses 121 | tile extent coordinates. The intersection geometry can be used for hierarchical 122 | processing, while the extent geometry is intended to be encoded as the tile geometry. 123 | Keep in mind that MVTs use local 'screen coordinates' with inverted y-axis compared with cartesian. 124 | 125 | ```java 126 | IGeometryFilter acceptAllGeomFilter = geometry -> true; 127 | Envelope tileEnvelope = new Envelope(0d, 100d, 0d, 100d); // TODO: Your tile extent here 128 | MvtLayerParams layerParams = new MvtLayerParams(); // Default extent 129 | 130 | TileGeomResult tileGeom = JtsAdapter.createTileGeom( 131 | jtsGeom, // Your geometry 132 | tileEnvelope, 133 | geomFactory, 134 | layerParams, 135 | acceptAllGeomFilter); 136 | ``` 137 | 138 | JavaDoc for JtsAdapter.createTileGeom() 139 | 140 | ```java 141 | /** 142 | * Create geometry clipped and then converted to MVT 'extent' coordinates. Result 143 | * contains both clipped geometry (intersection) and transformed geometry for encoding to MVT. 144 | * 145 | *

Uses the same tile and clipping coordinates. May cause rendering issues on boundaries for polygons 146 | * or line geometry depending on styling.

147 | * 148 | * @param g original 'source' geometry 149 | * @param tileEnvelope world coordinate bounds for tile 150 | * @param geomFactory creates a geometry for the tile envelope 151 | * @param mvtLayerParams specifies vector tile properties 152 | * @param filter geometry values that fail filter after transforms are removed 153 | * @return tile geometry result 154 | * @see TileGeomResult 155 | */ 156 | public static TileGeomResult createTileGeom(Geometry g, 157 | Envelope tileEnvelope, 158 | GeometryFactory geomFactory, 159 | MvtLayerParams mvtLayerParams, 160 | IGeometryFilter filter) 161 | ``` 162 | 163 | 164 | #### 3) Create MVT Builder, Layers, and Features 165 | 166 | After creating a tile's geometry in step 2, it is ready to be encoded in a MVT protobuf. 167 | 168 | Note: Applications can perform step 2 multiple times to place geometry in separate MVT layers. 169 | 170 | Create the VectorTile.Tile.Builder responsible for the MVT protobuf 171 | byte array. This is the top-level object for writing the MVT: 172 | 173 | ```java 174 | VectorTile.Tile.Builder tileBuilder = VectorTile.Tile.newBuilder(); 175 | ``` 176 | 177 | Create an empty layer for the MVT using the MvtLayerBuild#newLayerBuilder() utility function: 178 | 179 | ```java 180 | VectorTile.Tile.Layer.Builder layerBuilder = MvtLayerBuild.newLayerBuilder("myLayerName", layerParams); 181 | ``` 182 | 183 | MVT JTS Geometry from step 2 need to be converted to MVT features. 184 | 185 | MvtLayerProps is a supporting class for building MVT layer 186 | key/value dictionaries. A user data converter will take JTS Geometry 187 | user data (preserved during MVT tile geometry conversion) and convert it to MVT tags: 188 | 189 | ```java 190 | MvtLayerProps layerProps = new MvtLayerProps(); 191 | IUserDataConverter userDataConverter = new UserDataKeyValueMapConverter(); 192 | List features = JtsAdapter.toFeatures(tileGeom.mvtGeoms, layerProps, userDataConverter); 193 | ``` 194 | 195 | Use MvtLayerBuild#writeProps() utility function after JtsAdapter#toFeatures() to add the key/value dictionary to the 196 | MVT layer: 197 | 198 | ```java 199 | MvtLayerBuild.writeProps(layerBuilder, layerProps); 200 | ``` 201 | 202 | #### 4) Write MVT 203 | 204 | This example writes the MVT protobuf byte array to an output file. 205 | 206 | ```java 207 | VectorTile.Tile mvt = tileBuilder.build(); 208 | try { 209 | Files.write(path, mvt.toByteArray()); 210 | } catch (IOException e) { 211 | logger.error(e.getMessage(), e); 212 | } 213 | ``` 214 | 215 | ### Buffering Polygons Beyond MVT Extent 216 | 217 | For polygon geometry that will be styled with outlines, it is recommended that 218 | the clipping area be larger than the tile extent area. This can be handled like 219 | the example in MvtBuildTest#testBufferedPolygon(). Code example: 220 | 221 | ```java 222 | // Create input geometry 223 | final GeometryFactory geomFactory = new GeometryFactory(); 224 | final Geometry inputGeom = buildPolygon(RANDOM, 200, geomFactory); 225 | 226 | // Build tile envelope - 1 quadrant of the world 227 | final double tileWidth = WORLD_SIZE * .5d; 228 | final double tileHeight = WORLD_SIZE * .5d; 229 | final Envelope tileEnvelope = new Envelope(0d, tileWidth, 0d, tileHeight); 230 | 231 | // Build clip envelope - (10 * 2)% buffered area of the tile envelope 232 | final Envelope clipEnvelope = new Envelope(tileEnvelope); 233 | final double bufferWidth = tileWidth * .1f; 234 | final double bufferHeight = tileHeight * .1f; 235 | clipEnvelope.expandBy(bufferWidth, bufferHeight); 236 | 237 | // Build buffered MVT tile geometry 238 | final TileGeomResult bufferedTileGeom = JtsAdapter.createTileGeom( 239 | JtsAdapter.flatFeatureList(inputGeom), 240 | tileEnvelope, clipEnvelope, geomFactory, 241 | DEFAULT_MVT_PARAMS, ACCEPT_ALL_FILTER); 242 | 243 | // Create MVT layer 244 | final VectorTile.Tile mvt = encodeMvt(DEFAULT_MVT_PARAMS, bufferedTileGeom); 245 | ``` 246 | 247 | ## Examples 248 | 249 | See [tests](https://github.com/wdtinc/mapbox-vector-tile-java/tree/readme_upgrade/src/test/java/com/wdtinc/mapbox_vector_tile). 250 | 251 | ## How to generate VectorTile class using vector_tile.proto 252 | 253 | If vector_tile.proto is changed in the specification, VectorTile may need to be regenerated. 254 | 255 | Command `protoc` version should be the same version as the POM.xml dependency. 256 | 257 | protoc --java_out=src/main/java src/main/resources/vector_tile.proto 258 | 259 | #### Extra .proto config 260 | 261 | These options were added to the .proto file: 262 | 263 | * syntax = "proto2"; 264 | * option java_package = "com.wdtinc.mapbox_vector_tile"; 265 | * option java_outer_classname = "VectorTile"; 266 | 267 | ## Issues 268 | 269 | #### Reporting 270 | 271 | Use the Github issue tracker. 272 | 273 | #### Known Issues 274 | 275 | * Creating tile geometry with non-simple line strings that self-cross in many places will be 'noded' by JTS during an intersection operation. This results in ugly output. 276 | * Invalid or non-simple geometry may not work correctly with JTS operations when creating tile geometry. 277 | 278 | ## Contributing 279 | 280 | See [CONTRIBUTING](CONTRIBUTING.md) 281 | 282 | ## License 283 | 284 | http://www.apache.org/licenses/LICENSE-2.0.html 285 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /src/test/java/com/wdtinc/mapbox_vector_tile/build/MvtBuildTest.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.build; 2 | 3 | import org.locationtech.jts.algorithm.ConvexHull; 4 | import org.locationtech.jts.geom.*; 5 | import com.wdtinc.mapbox_vector_tile.VectorTile; 6 | import com.wdtinc.mapbox_vector_tile.adapt.jts.*; 7 | import com.wdtinc.mapbox_vector_tile.adapt.jts.model.JtsLayer; 8 | import com.wdtinc.mapbox_vector_tile.adapt.jts.model.JtsMvt; 9 | import com.wdtinc.mapbox_vector_tile.util.JdkUtils; 10 | import org.junit.Test; 11 | 12 | import java.io.ByteArrayInputStream; 13 | import java.io.IOException; 14 | import java.util.*; 15 | 16 | import static java.util.Collections.singletonList; 17 | import static org.junit.Assert.*; 18 | 19 | /** 20 | * Test building MVTs. 21 | */ 22 | public final class MvtBuildTest { 23 | 24 | private static String TEST_LAYER_NAME = "layerNameHere"; 25 | 26 | /** 27 | * Fixed randomization with arbitrary seed value. 28 | */ 29 | private static final long SEED = 487125064L; 30 | 31 | /** 32 | * Fixed random. 33 | */ 34 | private static final Random RANDOM = new Random(SEED); 35 | 36 | /** 37 | * Example world is 100x100 box. 38 | */ 39 | private static final double WORLD_SIZE = 100D; 40 | 41 | /** 42 | * Do not filter tile geometry. 43 | */ 44 | private static final IGeometryFilter ACCEPT_ALL_FILTER = geometry -> true; 45 | 46 | /** 47 | * Default MVT parameters. 48 | */ 49 | private static final MvtLayerParams DEFAULT_MVT_PARAMS = new MvtLayerParams(); 50 | 51 | /** 52 | * Generate Geometries with this default specification. 53 | */ 54 | private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory(); 55 | 56 | @Test 57 | public void testPoints() throws IOException { 58 | 59 | // Create input geometry 60 | final GeometryFactory geomFactory = new GeometryFactory(); 61 | final Geometry inputGeom = buildMultiPoint(RANDOM, 200, geomFactory); 62 | 63 | // Build tile envelope - 1 quadrant of the world 64 | final Envelope tileEnvelope = new Envelope(0d, WORLD_SIZE * .5d, 0d, WORLD_SIZE * .5d); 65 | 66 | // Build MVT tile geometry 67 | final TileGeomResult tileGeom = JtsAdapter.createTileGeom(inputGeom, tileEnvelope, geomFactory, 68 | DEFAULT_MVT_PARAMS, ACCEPT_ALL_FILTER); 69 | 70 | final VectorTile.Tile mvt = encodeMvt(DEFAULT_MVT_PARAMS, tileGeom); 71 | 72 | // MVT Bytes 73 | final byte[] bytes = mvt.toByteArray(); 74 | 75 | assertNotNull(bytes); 76 | 77 | JtsMvt expected = new JtsMvt(singletonList(new JtsLayer(TEST_LAYER_NAME, tileGeom.mvtGeoms))); 78 | 79 | // Load multipolygon z0 tile 80 | JtsMvt actual = MvtReader.loadMvt( 81 | new ByteArrayInputStream(bytes), 82 | new GeometryFactory(), 83 | new TagKeyValueMapConverter()); 84 | 85 | // Check that MVT geometries are the same as the ones that were encoded above 86 | assertEquals(expected, actual); 87 | } 88 | 89 | @Test 90 | public void testLines() throws IOException { 91 | 92 | // Create input geometry 93 | final GeometryFactory geomFactory = new GeometryFactory(); 94 | final Geometry inputGeom = buildLineString(RANDOM, 10, geomFactory); 95 | 96 | // Build tile envelope - 1 quadrant of the world 97 | final Envelope tileEnvelope = new Envelope(0d, WORLD_SIZE * .5d, 0d, WORLD_SIZE * .5d); 98 | 99 | // Build MVT tile geometry 100 | final TileGeomResult tileGeom = JtsAdapter.createTileGeom(inputGeom, tileEnvelope, geomFactory, 101 | DEFAULT_MVT_PARAMS, ACCEPT_ALL_FILTER); 102 | 103 | // Create MVT layer 104 | final VectorTile.Tile mvt = encodeMvt(DEFAULT_MVT_PARAMS, tileGeom); 105 | 106 | // MVT Bytes 107 | final byte[] bytes = mvt.toByteArray(); 108 | 109 | assertNotNull(bytes); 110 | 111 | JtsMvt expected = new JtsMvt(singletonList(new JtsLayer(TEST_LAYER_NAME, tileGeom.mvtGeoms))); 112 | 113 | // Load multipolygon z0 tile 114 | JtsMvt actual = MvtReader.loadMvt( 115 | new ByteArrayInputStream(bytes), 116 | new GeometryFactory(), 117 | new TagKeyValueMapConverter()); 118 | 119 | // Check that MVT geometries are the same as the ones that were encoded above 120 | assertEquals(expected, actual); 121 | } 122 | 123 | @Test 124 | public void testPolygon() throws IOException { 125 | 126 | // Create input geometry 127 | final GeometryFactory geomFactory = new GeometryFactory(); 128 | final Geometry inputGeom = buildPolygon(RANDOM, 200, geomFactory); 129 | 130 | // Build tile envelope - 1 quadrant of the world 131 | final Envelope tileEnvelope = new Envelope(0d, WORLD_SIZE * .5d, 0d, WORLD_SIZE * .5d); 132 | 133 | // Build MVT tile geometry 134 | final TileGeomResult tileGeom = JtsAdapter.createTileGeom(inputGeom, tileEnvelope, geomFactory, 135 | DEFAULT_MVT_PARAMS, ACCEPT_ALL_FILTER); 136 | 137 | // Create MVT layer 138 | final VectorTile.Tile mvt = encodeMvt(DEFAULT_MVT_PARAMS, tileGeom); 139 | 140 | // MVT Bytes 141 | final byte[] bytes = mvt.toByteArray(); 142 | 143 | assertNotNull(bytes); 144 | 145 | JtsMvt expected = new JtsMvt(singletonList(new JtsLayer(TEST_LAYER_NAME, tileGeom.mvtGeoms))); 146 | 147 | // Load multipolygon z0 tile 148 | JtsMvt actual = MvtReader.loadMvt( 149 | new ByteArrayInputStream(bytes), 150 | new GeometryFactory(), 151 | new TagKeyValueMapConverter()); 152 | 153 | // Check that MVT geometries are the same as the ones that were encoded above 154 | assertEquals(expected, actual); 155 | } 156 | 157 | @Test 158 | public void testBufferedPolygon() throws IOException { 159 | 160 | // Create input geometry 161 | final GeometryFactory geomFactory = new GeometryFactory(); 162 | final Geometry inputGeom = buildPolygon(RANDOM, 200, geomFactory); 163 | 164 | // Build tile envelope - 1 quadrant of the world 165 | final double tileWidth = WORLD_SIZE * .5d; 166 | final double tileHeight = WORLD_SIZE * .5d; 167 | final Envelope tileEnvelope = new Envelope(0d, tileWidth, 0d, tileHeight); 168 | 169 | // Build clip envelope - (10 * 2)% buffered area of the tile envelope 170 | final Envelope clipEnvelope = new Envelope(tileEnvelope); 171 | final double bufferWidth = tileWidth * .1f; 172 | final double bufferHeight = tileHeight * .1f; 173 | clipEnvelope.expandBy(bufferWidth, bufferHeight); 174 | 175 | // Build buffered MVT tile geometry 176 | final TileGeomResult bufferedTileGeom = JtsAdapter.createTileGeom( 177 | JtsAdapter.flatFeatureList(inputGeom), 178 | tileEnvelope, clipEnvelope, geomFactory, 179 | DEFAULT_MVT_PARAMS, ACCEPT_ALL_FILTER); 180 | 181 | // Create MVT layer 182 | final VectorTile.Tile mvt = encodeMvt(DEFAULT_MVT_PARAMS, bufferedTileGeom); 183 | 184 | // MVT Bytes 185 | final byte[] bytes = mvt.toByteArray(); 186 | 187 | assertNotNull(bytes); 188 | 189 | JtsMvt expected = new JtsMvt(singletonList( 190 | new JtsLayer(TEST_LAYER_NAME, bufferedTileGeom.mvtGeoms))); 191 | 192 | // Load multipolygon z0 tile 193 | JtsMvt actual = MvtReader.loadMvt( 194 | new ByteArrayInputStream(bytes), 195 | new GeometryFactory(), 196 | new TagKeyValueMapConverter()); 197 | 198 | // Check that MVT geometries are the same as the ones that were encoded above 199 | assertEquals(expected, actual); 200 | } 201 | 202 | @Test 203 | public void testPointsInLayers() throws IOException { 204 | Point point1 = createPoint(); 205 | Point point2 = createPoint(); 206 | Point point3 = createPoint(); 207 | 208 | String layer1Name = "Layer 1"; 209 | String layer2Name = "Layer 2"; 210 | 211 | byte[] bytes = new MvtWriter.Builder() 212 | .setLayer(layer1Name) 213 | .add(point1) 214 | .add(point2) 215 | .setLayer(layer2Name) 216 | .add(point3) 217 | .build(); 218 | 219 | assertNotNull(bytes); 220 | 221 | JtsMvt layers = MvtReader.loadMvt(new ByteArrayInputStream(bytes), new GeometryFactory(), 222 | new TagKeyValueMapConverter()); 223 | 224 | assertNotNull(layers.getLayer(layer1Name)); 225 | assertNotNull(layers.getLayer(layer2Name)); 226 | 227 | Collection actualLayer1Geometries = layers.getLayer(layer1Name).getGeometries(); 228 | Collection expectedLayer1Geometries = Arrays.asList(point1, point2); 229 | assertEquals(expectedLayer1Geometries, actualLayer1Geometries); 230 | 231 | Collection actualLayer2Geometries = layers.getLayer(layer2Name).getGeometries(); 232 | Collection expectedLayer2Geometries = Arrays.asList(point3); 233 | assertEquals(expectedLayer2Geometries, actualLayer2Geometries); 234 | } 235 | 236 | private static MultiPoint buildMultiPoint(Random random, int pointCount, GeometryFactory geomFactory) { 237 | final CoordinateSequence coordSeq = getCoordSeq(random, pointCount, geomFactory); 238 | return geomFactory.createMultiPoint(coordSeq); 239 | } 240 | 241 | private static LineString buildLineString(Random random, int pointCount, GeometryFactory geomFactory) { 242 | final CoordinateSequence coordSeq = getCoordSeq(random, pointCount, geomFactory); 243 | return new LineString(coordSeq, geomFactory); 244 | } 245 | 246 | private static Polygon buildPolygon(Random random, int pointCount, GeometryFactory geomFactory) { 247 | if(pointCount < 3) { 248 | throw new RuntimeException("Need 3 or more points to be a polygon"); 249 | } 250 | final CoordinateSequence coordSeq = getCoordSeq(random, pointCount, geomFactory); 251 | final ConvexHull convexHull = new ConvexHull(coordSeq.toCoordinateArray(), geomFactory); 252 | final Geometry hullGeom = convexHull.getConvexHull(); 253 | return (Polygon) hullGeom; 254 | } 255 | 256 | private Point createPoint() { 257 | Coordinate coord = new Coordinate(RANDOM.nextInt(4096), RANDOM.nextInt(4096)); 258 | Point point = GEOMETRY_FACTORY.createPoint(coord); 259 | 260 | Map attributes = new LinkedHashMap<>(); 261 | attributes.put("id", RANDOM.nextDouble()); 262 | attributes.put("name", String.format("name %f : %f", coord.x, coord.y)); 263 | point.setUserData(attributes); 264 | 265 | return point; 266 | } 267 | 268 | private static CoordinateSequence getCoordSeq(Random random, int pointCount, GeometryFactory geomFactory) { 269 | final CoordinateSequence coordSeq = geomFactory.getCoordinateSequenceFactory().create(pointCount, 2); 270 | for(int i = 0; i < pointCount; ++i) { 271 | final Coordinate coord = coordSeq.getCoordinate(i); 272 | coord.setOrdinate(0, random.nextDouble() * WORLD_SIZE); 273 | coord.setOrdinate(1, random.nextDouble() * WORLD_SIZE); 274 | } 275 | return coordSeq; 276 | } 277 | 278 | private static VectorTile.Tile encodeMvt(MvtLayerParams mvtParams, TileGeomResult tileGeom) { 279 | 280 | // Build MVT 281 | final VectorTile.Tile.Builder tileBuilder = VectorTile.Tile.newBuilder(); 282 | 283 | // Create MVT layer 284 | final VectorTile.Tile.Layer.Builder layerBuilder = MvtLayerBuild.newLayerBuilder(TEST_LAYER_NAME, mvtParams); 285 | final MvtLayerProps layerProps = new MvtLayerProps(); 286 | final UserDataIgnoreConverter ignoreUserData = new UserDataIgnoreConverter(); 287 | 288 | // MVT tile geometry to MVT features 289 | final List features = JtsAdapter.toFeatures(tileGeom.mvtGeoms, layerProps, ignoreUserData); 290 | layerBuilder.addAllFeatures(features); 291 | MvtLayerBuild.writeProps(layerBuilder, layerProps); 292 | 293 | // Build MVT layer 294 | final VectorTile.Tile.Layer layer = layerBuilder.build(); 295 | 296 | // Add built layer to MVT 297 | tileBuilder.addLayers(layer); 298 | 299 | /// Build MVT 300 | return tileBuilder.build(); 301 | } 302 | 303 | private static class MvtWriter { 304 | 305 | static class Builder { 306 | // Default MVT parameters 307 | private static final MvtLayerParams DEFAULT_MVT_PARAMS = new MvtLayerParams(); 308 | 309 | private String activeLayer = "default"; 310 | 311 | private Map> layers = new HashMap<>(); 312 | 313 | Builder() {} 314 | 315 | Builder setLayer(String layerName) { 316 | JdkUtils.requireNonNull(layerName); 317 | activeLayer = layerName; 318 | return this; 319 | } 320 | 321 | Builder add(Geometry geometry) { 322 | JdkUtils.requireNonNull(geometry); 323 | getActiveLayer().add(geometry); 324 | return this; 325 | } 326 | 327 | byte[] build() { 328 | // Build MVT 329 | final VectorTile.Tile.Builder tileBuilder = VectorTile.Tile.newBuilder(); 330 | 331 | for (Map.Entry> layer : layers.entrySet()) { 332 | // Layer 333 | String name = layer.getKey(); 334 | List geometries = layer.getValue(); 335 | 336 | // Create MVT layer 337 | final VectorTile.Tile.Layer.Builder layerBuilder = 338 | MvtLayerBuild.newLayerBuilder(name, DEFAULT_MVT_PARAMS); 339 | 340 | final MvtLayerProps layerProps = new MvtLayerProps(); 341 | 342 | // MVT tile geometry to MVT features 343 | final List features = 344 | JtsAdapter.toFeatures(geometries, layerProps, 345 | new UserDataKeyValueMapConverter()); 346 | 347 | layerBuilder.addAllFeatures(features); 348 | MvtLayerBuild.writeProps(layerBuilder, layerProps); 349 | 350 | // Build MVT layer 351 | final VectorTile.Tile.Layer mvtLayer = layerBuilder.build(); 352 | tileBuilder.addLayers(mvtLayer); 353 | } 354 | 355 | // Build MVT 356 | return tileBuilder.build().toByteArray(); 357 | } 358 | 359 | private List getActiveLayer() { 360 | boolean isDefined = layers.containsKey(activeLayer); 361 | if (!isDefined) { 362 | layers.put(activeLayer, new ArrayList<>()); 363 | } 364 | return layers.get(activeLayer); 365 | } 366 | } 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/adapt/jts/MvtReader.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.adapt.jts; 2 | 3 | import org.locationtech.jts.algorithm.Area; 4 | import org.locationtech.jts.geom.*; 5 | import com.wdtinc.mapbox_vector_tile.adapt.jts.model.JtsLayer; 6 | import com.wdtinc.mapbox_vector_tile.adapt.jts.model.JtsMvt; 7 | import com.wdtinc.mapbox_vector_tile.encoding.GeomCmd; 8 | import com.wdtinc.mapbox_vector_tile.VectorTile; 9 | import com.wdtinc.mapbox_vector_tile.encoding.GeomCmdHdr; 10 | import com.wdtinc.mapbox_vector_tile.encoding.ZigZag; 11 | import com.wdtinc.mapbox_vector_tile.util.Vec2d; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import java.io.File; 15 | import java.io.FileInputStream; 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | /** 22 | * Load Mapbox Vector Tiles (MVT) to JTS {@link Geometry}. Feature tags may be converted 23 | * to user data via {@link ITagConverter}. 24 | * 25 | * @see JtsMvt 26 | * @see JtsLayer 27 | */ 28 | public final class MvtReader { 29 | private static final int MIN_LINE_STRING_LEN = 6; // MoveTo,1 + LineTo,1 30 | private static final int MIN_POLYGON_LEN = 9; // MoveTo,1 + LineTo,2 + ClosePath 31 | 32 | /** 33 | * Convenience method for loading MVT from file. 34 | * See {@link #loadMvt(InputStream, GeometryFactory, ITagConverter, RingClassifier)}. 35 | * Uses {@link #RING_CLASSIFIER_V2_1} for forming Polygons and MultiPolygons. 36 | * 37 | * @param file path to the MVT 38 | * @param geomFactory allows for JTS geometry creation 39 | * @param tagConverter converts MVT feature tags to JTS user data object 40 | * @return JTS MVT with geometry in MVT coordinates 41 | * @throws IOException failure reading MVT from path 42 | * @see #loadMvt(InputStream, GeometryFactory, ITagConverter, RingClassifier) 43 | * @see Geometry 44 | * @see Geometry#getUserData() 45 | * @see RingClassifier 46 | */ 47 | public static JtsMvt loadMvt(File file, 48 | GeometryFactory geomFactory, 49 | ITagConverter tagConverter) throws IOException { 50 | return loadMvt(file, geomFactory, tagConverter, RING_CLASSIFIER_DEFAULT); 51 | } 52 | 53 | /** 54 | * Convenience method for loading MVT from file. 55 | * See {@link #loadMvt(InputStream, GeometryFactory, ITagConverter, RingClassifier)}. 56 | * 57 | * @param file path to the MVT 58 | * @param geomFactory allows for JTS geometry creation 59 | * @param tagConverter converts MVT feature tags to JTS user data object 60 | * @param ringClassifier determines how rings are parsed into Polygons and MultiPolygons 61 | * @return JTS MVT with geometry in MVT coordinates 62 | * @throws IOException failure reading MVT from path 63 | * @see #loadMvt(InputStream, GeometryFactory, ITagConverter, RingClassifier) 64 | * @see Geometry 65 | * @see Geometry#getUserData() 66 | * @see RingClassifier 67 | */ 68 | public static JtsMvt loadMvt(File file, 69 | GeometryFactory geomFactory, 70 | ITagConverter tagConverter, 71 | RingClassifier ringClassifier) throws IOException { 72 | final JtsMvt jtsMvt; 73 | 74 | try(final InputStream is = new FileInputStream(file)) { 75 | jtsMvt = loadMvt(is, geomFactory, tagConverter, ringClassifier); 76 | } 77 | 78 | return jtsMvt; 79 | } 80 | 81 | /** 82 | * Load an MVT to JTS geometries using coordinates. Uses {@code tagConverter} to create user data 83 | * from feature properties. 84 | * 85 | * @param is stream with MVT data 86 | * @param geomFactory allows for JTS geometry creation 87 | * @param tagConverter converts MVT feature tags to JTS user data object. 88 | * @return JTS MVT with geometry in MVT coordinates 89 | * @throws IOException failure reading MVT from stream 90 | * @see Geometry 91 | * @see Geometry#getUserData() 92 | * @see RingClassifier 93 | */ 94 | public static JtsMvt loadMvt(InputStream is, 95 | GeometryFactory geomFactory, 96 | ITagConverter tagConverter) throws IOException { 97 | return loadMvt(is, geomFactory, tagConverter, RING_CLASSIFIER_DEFAULT); 98 | } 99 | 100 | /** 101 | * Load an MVT to JTS geometries using coordinates. Uses {@code tagConverter} to create user data 102 | * from feature properties. 103 | * 104 | * @param is stream with MVT data 105 | * @param geomFactory allows for JTS geometry creation 106 | * @param tagConverter converts MVT feature tags to JTS user data object. 107 | * @param ringClassifier determines how rings are parsed into Polygons and MultiPolygons 108 | * @return JTS MVT with geometry in MVT coordinates 109 | * @throws IOException failure reading MVT from stream 110 | * @see Geometry 111 | * @see Geometry#getUserData() 112 | * @see RingClassifier 113 | */ 114 | public static JtsMvt loadMvt(InputStream is, 115 | GeometryFactory geomFactory, 116 | ITagConverter tagConverter, 117 | RingClassifier ringClassifier) throws IOException { 118 | 119 | final VectorTile.Tile mvt = VectorTile.Tile.parseFrom(is); 120 | final Vec2d cursor = new Vec2d(); 121 | final List jtsLayers = new ArrayList<>(mvt.getLayersList().size()); 122 | 123 | for(VectorTile.Tile.Layer nextLayer : mvt.getLayersList()) { 124 | 125 | final List keysList = nextLayer.getKeysList(); 126 | final List valuesList = nextLayer.getValuesList(); 127 | final List layerGeoms = new ArrayList<>(nextLayer.getFeaturesList().size()); 128 | 129 | for(VectorTile.Tile.Feature nextFeature : nextLayer.getFeaturesList()) { 130 | 131 | final Long id = nextFeature.hasId() ? nextFeature.getId() : null; 132 | 133 | final VectorTile.Tile.GeomType geomType = nextFeature.getType(); 134 | 135 | if(geomType == VectorTile.Tile.GeomType.UNKNOWN) { 136 | continue; 137 | } 138 | 139 | final List geomCmds = nextFeature.getGeometryList(); 140 | cursor.set(0d, 0d); 141 | final Geometry nextGeom = readGeometry(geomCmds, geomType, geomFactory, cursor, ringClassifier); 142 | if(nextGeom != null) { 143 | nextGeom.setUserData(tagConverter.toUserData(id, nextFeature.getTagsList(), keysList, valuesList)); 144 | layerGeoms.add(nextGeom); 145 | } 146 | } 147 | 148 | jtsLayers.add(new JtsLayer(nextLayer.getName(), layerGeoms, nextLayer.getExtent())); 149 | } 150 | 151 | 152 | return new JtsMvt(jtsLayers); 153 | } 154 | 155 | private static Geometry readGeometry(List geomCmds, 156 | VectorTile.Tile.GeomType geomType, 157 | GeometryFactory geomFactory, 158 | Vec2d cursor, 159 | RingClassifier ringClassifier) { 160 | Geometry result = null; 161 | 162 | switch(geomType) { 163 | case POINT: 164 | result = readPoints(geomFactory, geomCmds, cursor); 165 | break; 166 | case LINESTRING: 167 | result = readLines(geomFactory, geomCmds, cursor); 168 | break; 169 | case POLYGON: 170 | result = readPolys(geomFactory, geomCmds, cursor, ringClassifier); 171 | break; 172 | default: 173 | LoggerFactory.getLogger(MvtReader.class).error("readGeometry(): Unhandled geometry type [{}]", geomType); 174 | } 175 | 176 | return result; 177 | } 178 | 179 | /** 180 | * Create {@link Point} or {@link MultiPoint} from MVT geometry drawing commands. 181 | * 182 | * @param geomFactory creates JTS geometry 183 | * @param geomCmds contains MVT geometry commands 184 | * @param cursor contains current MVT extent position 185 | * @return JTS geometry or null on failure 186 | */ 187 | private static Geometry readPoints(GeometryFactory geomFactory, List geomCmds, Vec2d cursor) { 188 | 189 | // Guard: must have header 190 | if(geomCmds.isEmpty()) { 191 | return null; 192 | } 193 | 194 | /** Geometry command index */ 195 | int i = 0; 196 | 197 | // Read command header 198 | final int cmdHdr = geomCmds.get(i++); 199 | final int cmdLength = GeomCmdHdr.getCmdLength(cmdHdr); 200 | final GeomCmd cmd = GeomCmdHdr.getCmd(cmdHdr); 201 | 202 | // Guard: command type 203 | if(cmd != GeomCmd.MoveTo) { 204 | return null; 205 | } 206 | 207 | // Guard: minimum command length 208 | if(cmdLength < 1) { 209 | return null; 210 | } 211 | 212 | // Guard: header data unsupported by geometry command buffer 213 | // (require header and at least 1 value * 2 params) 214 | if(cmdLength * GeomCmd.MoveTo.getParamCount() + 1 > geomCmds.size()) { 215 | return null; 216 | } 217 | 218 | final CoordinateSequence coordSeq = geomFactory.getCoordinateSequenceFactory().create(cmdLength, 2); 219 | int coordIndex = 0; 220 | 221 | while(i < geomCmds.size() - 1) { 222 | cursor.add( 223 | ZigZag.decode(geomCmds.get(i++)), 224 | ZigZag.decode(geomCmds.get(i++)) 225 | ); 226 | 227 | coordSeq.setOrdinate(coordIndex, 0, cursor.x); 228 | coordSeq.setOrdinate(coordIndex, 1, cursor.y); 229 | coordIndex++; 230 | } 231 | 232 | return coordSeq.size() == 1 ? geomFactory.createPoint(coordSeq) : geomFactory.createMultiPoint(coordSeq); 233 | } 234 | 235 | /** 236 | * Create {@link LineString} or {@link MultiLineString} from MVT geometry drawing commands. 237 | * 238 | * @param geomFactory creates JTS geometry 239 | * @param geomCmds contains MVT geometry commands 240 | * @param cursor contains current MVT extent position 241 | * @return JTS geometry or null on failure 242 | */ 243 | private static Geometry readLines(GeometryFactory geomFactory, List geomCmds, Vec2d cursor) { 244 | 245 | // Guard: must have header 246 | if(geomCmds.isEmpty()) { 247 | return null; 248 | } 249 | 250 | /** Geometry command index */ 251 | int i = 0; 252 | 253 | int cmdHdr; 254 | int cmdLength; 255 | GeomCmd cmd; 256 | List geoms = new ArrayList<>(1); 257 | CoordinateSequence nextCoordSeq; 258 | 259 | while(i <= geomCmds.size() - MIN_LINE_STRING_LEN) { 260 | 261 | // -------------------------------------------- 262 | // Expected: MoveTo command of length 1 263 | // -------------------------------------------- 264 | 265 | // Read command header 266 | cmdHdr = geomCmds.get(i++); 267 | cmdLength = GeomCmdHdr.getCmdLength(cmdHdr); 268 | cmd = GeomCmdHdr.getCmd(cmdHdr); 269 | 270 | // Guard: command type and length 271 | if(cmd != GeomCmd.MoveTo || cmdLength != 1) { 272 | break; 273 | } 274 | 275 | // Update cursor position with relative move 276 | cursor.add( 277 | ZigZag.decode(geomCmds.get(i++)), 278 | ZigZag.decode(geomCmds.get(i++)) 279 | ); 280 | 281 | 282 | // -------------------------------------------- 283 | // Expected: LineTo command of length > 0 284 | // -------------------------------------------- 285 | 286 | // Read command header 287 | cmdHdr = geomCmds.get(i++); 288 | cmdLength = GeomCmdHdr.getCmdLength(cmdHdr); 289 | cmd = GeomCmdHdr.getCmd(cmdHdr); 290 | 291 | // Guard: command type and length 292 | if(cmd != GeomCmd.LineTo || cmdLength < 1) { 293 | break; 294 | } 295 | 296 | // Guard: header data length unsupported by geometry command buffer 297 | // (require at least (1 value * 2 params) + current_index) 298 | if((cmdLength * GeomCmd.LineTo.getParamCount()) + i > geomCmds.size()) { 299 | break; 300 | } 301 | 302 | nextCoordSeq = geomFactory.getCoordinateSequenceFactory().create(1 + cmdLength, 2); 303 | 304 | // Set first point from MoveTo command 305 | nextCoordSeq.setOrdinate(0, 0, cursor.x); 306 | nextCoordSeq.setOrdinate(0, 1, cursor.y); 307 | 308 | // Set remaining points from LineTo command 309 | for(int lineToIndex = 0; lineToIndex < cmdLength; ++lineToIndex) { 310 | 311 | // Update cursor position with relative line delta 312 | cursor.add( 313 | ZigZag.decode(geomCmds.get(i++)), 314 | ZigZag.decode(geomCmds.get(i++)) 315 | ); 316 | 317 | nextCoordSeq.setOrdinate(lineToIndex + 1, 0, cursor.x); 318 | nextCoordSeq.setOrdinate(lineToIndex + 1, 1, cursor.y); 319 | } 320 | 321 | geoms.add(geomFactory.createLineString(nextCoordSeq)); 322 | } 323 | 324 | return geoms.size() == 1 ? geoms.get(0) : geomFactory.createMultiLineString(geoms.toArray(new LineString[geoms.size()])); 325 | } 326 | 327 | /** 328 | * Create {@link Polygon} or {@link MultiPolygon} from MVT geometry drawing commands. 329 | * 330 | * @param geomFactory creates JTS geometry 331 | * @param geomCmds contains MVT geometry commands 332 | * @param cursor contains current MVT extent position 333 | * @param ringClassifier 334 | * @return JTS geometry or null on failure 335 | */ 336 | private static Geometry readPolys(GeometryFactory geomFactory, 337 | List geomCmds, 338 | Vec2d cursor, 339 | RingClassifier ringClassifier) { 340 | 341 | // Guard: must have header 342 | if(geomCmds.isEmpty()) { 343 | return null; 344 | } 345 | 346 | /** Geometry command index */ 347 | int i = 0; 348 | 349 | int cmdHdr; 350 | int cmdLength; 351 | GeomCmd cmd; 352 | List rings = new ArrayList<>(1); 353 | CoordinateSequence nextCoordSeq; 354 | 355 | while(i <= geomCmds.size() - MIN_POLYGON_LEN) { 356 | 357 | // -------------------------------------------- 358 | // Expected: MoveTo command of length 1 359 | // -------------------------------------------- 360 | 361 | // Read command header 362 | cmdHdr = geomCmds.get(i++); 363 | cmdLength = GeomCmdHdr.getCmdLength(cmdHdr); 364 | cmd = GeomCmdHdr.getCmd(cmdHdr); 365 | 366 | // Guard: command type and length 367 | if(cmd != GeomCmd.MoveTo || cmdLength != 1) { 368 | break; 369 | } 370 | 371 | // Update cursor position with relative move 372 | cursor.add( 373 | ZigZag.decode(geomCmds.get(i++)), 374 | ZigZag.decode(geomCmds.get(i++)) 375 | ); 376 | 377 | 378 | // -------------------------------------------- 379 | // Expected: LineTo command of length > 1 380 | // -------------------------------------------- 381 | 382 | // Read command header 383 | cmdHdr = geomCmds.get(i++); 384 | cmdLength = GeomCmdHdr.getCmdLength(cmdHdr); 385 | cmd = GeomCmdHdr.getCmd(cmdHdr); 386 | 387 | // Guard: command type and length 388 | if(cmd != GeomCmd.LineTo || cmdLength < 2) { 389 | break; 390 | } 391 | 392 | // Guard: header data length unsupported by geometry command buffer 393 | // (require at least (2 values * 2 params) + (current index 'i') + (1 for ClosePath)) 394 | if((cmdLength * GeomCmd.LineTo.getParamCount()) + i + 1 > geomCmds.size()) { 395 | break; 396 | } 397 | 398 | nextCoordSeq = geomFactory.getCoordinateSequenceFactory().create(2 + cmdLength, 2); 399 | 400 | // Set first point from MoveTo command 401 | nextCoordSeq.setOrdinate(0, 0, cursor.x); 402 | nextCoordSeq.setOrdinate(0, 1, cursor.y); 403 | 404 | // Set remaining points from LineTo command 405 | for(int lineToIndex = 0; lineToIndex < cmdLength; ++lineToIndex) { 406 | 407 | // Update cursor position with relative line delta 408 | cursor.add( 409 | ZigZag.decode(geomCmds.get(i++)), 410 | ZigZag.decode(geomCmds.get(i++)) 411 | ); 412 | 413 | nextCoordSeq.setOrdinate(lineToIndex + 1, 0, cursor.x); 414 | nextCoordSeq.setOrdinate(lineToIndex + 1, 1, cursor.y); 415 | } 416 | 417 | 418 | // -------------------------------------------- 419 | // Expected: ClosePath command of length 1 420 | // -------------------------------------------- 421 | 422 | // Read command header 423 | cmdHdr = geomCmds.get(i++); 424 | cmdLength = GeomCmdHdr.getCmdLength(cmdHdr); 425 | cmd = GeomCmdHdr.getCmd(cmdHdr); 426 | 427 | if(cmd != GeomCmd.ClosePath || cmdLength != 1) { 428 | break; 429 | } 430 | 431 | // Set last point from ClosePath command 432 | nextCoordSeq.setOrdinate(nextCoordSeq.size() - 1, 0, nextCoordSeq.getOrdinate(0, 0)); 433 | nextCoordSeq.setOrdinate(nextCoordSeq.size() - 1, 1, nextCoordSeq.getOrdinate(0, 1)); 434 | 435 | rings.add(geomFactory.createLinearRing(nextCoordSeq)); 436 | } 437 | 438 | 439 | // Classify rings 440 | final List polygons = ringClassifier.classifyRings(rings, geomFactory); 441 | if(polygons.size() < 1) { 442 | return null; 443 | 444 | } else if(polygons.size() == 1) { 445 | return polygons.get(0); 446 | 447 | } else { 448 | return geomFactory.createMultiPolygon(polygons.toArray(new Polygon[polygons.size()])); 449 | } 450 | } 451 | 452 | 453 | /** 454 | * Classifies Polygon and MultiPolygon rings. 455 | */ 456 | public interface RingClassifier { 457 | 458 | /** 459 | *

Classify a list of rings into polygons using surveyor formula.

460 | *

Zero-area polygons are removed.

461 | * 462 | * @param rings linear rings to classify into polygons 463 | * @param geomFactory creates JTS geometry 464 | * @return polygons from classified rings 465 | */ 466 | List classifyRings(List rings, GeometryFactory geomFactory); 467 | } 468 | 469 | 470 | /** 471 | * Area for surveyor formula may be positive or negative for exterior rings. Mimics Mapbox parsers supporting V1. 472 | */ 473 | public static final RingClassifier RING_CLASSIFIER_V1 = new PolyRingClassifierV1(); 474 | 475 | /** 476 | * Area from surveyor formula must be positive for exterior rings. Obeys V2.1 spec. 477 | */ 478 | public static final RingClassifier RING_CLASSIFIER_V2_1 = new PolyRingClassifierV2_1(); 479 | 480 | /** 481 | * Default ring classifier when it is not specified. 482 | */ 483 | private static final RingClassifier RING_CLASSIFIER_DEFAULT = RING_CLASSIFIER_V1; 484 | 485 | 486 | /** 487 | * Area from surveyor formula must be positive for exterior rings (but area check is flipped because MVT is Y-DOWN). 488 | * Obeys V2.1 spec. 489 | * 490 | * @see Area#ofRingSigned(Coordinate[]) 491 | */ 492 | private static final class PolyRingClassifierV2_1 implements RingClassifier { 493 | 494 | @Override 495 | public List classifyRings(List rings, GeometryFactory geomFactory) { 496 | final List polygons = new ArrayList<>(); 497 | final List holes = new ArrayList<>(); 498 | 499 | double outerArea = 0d; 500 | LinearRing outerPoly = null; 501 | 502 | for(LinearRing r : rings) { 503 | 504 | // Area.ofRingSigned() area is positive if the ring is oriented CW, negative if the 505 | // ring is oriented CCW, and zero if the ring is degenerate or flat 506 | double area = Area.ofRingSigned(r.getCoordinates()); 507 | 508 | if(!r.isRing()) { 509 | continue; // sanity check, could probably be handled in a isSimple() check 510 | } 511 | 512 | if(area == 0d) { 513 | continue; // zero-area 514 | } 515 | 516 | if(area < 0d) { 517 | if(outerPoly != null) { 518 | polygons.add(geomFactory.createPolygon(outerPoly, holes.toArray(new LinearRing[holes.size()]))); 519 | holes.clear(); 520 | } 521 | 522 | // Neg (in Y-DOWN MVT) --> Pos CW, Outer 523 | outerPoly = r; 524 | outerArea = area; 525 | 526 | } else { 527 | 528 | if(Math.abs(outerArea) < Math.abs(area)) { 529 | continue; // Holes must have less area, could probably be handled in a isSimple() check 530 | } 531 | 532 | // Pos (in Y-DOWN MVT) --> Neg CCW, Hole 533 | holes.add(r); 534 | } 535 | } 536 | 537 | if(outerPoly != null) { 538 | polygons.add(geomFactory.createPolygon(outerPoly, holes.toArray(new LinearRing[holes.size()]))); 539 | } 540 | 541 | return polygons; 542 | } 543 | } 544 | 545 | 546 | /** 547 | * Area for surveyor formula may be positive or negative for exterior rings. Mimics Mapbox parsers supporting V1. The 548 | * outer ring winding order is established by the first polygon ring. 549 | * 550 | * @see Area#ofRingSigned(Coordinate[]) 551 | */ 552 | private static final class PolyRingClassifierV1 implements RingClassifier { 553 | 554 | @Override 555 | public List classifyRings(List rings, GeometryFactory geomFactory) { 556 | final List polygons = new ArrayList<>(); 557 | final List holes = new ArrayList<>(); 558 | 559 | double outerArea = 0d; 560 | LinearRing outerPoly = null; 561 | 562 | for(LinearRing r : rings) { 563 | 564 | // Area.ofRingSigned() area is positive if the ring is oriented CW, negative if the 565 | // ring is oriented CCW, and zero if the ring is degenerate or flat 566 | double area = Area.ofRingSigned(r.getCoordinates()); 567 | 568 | if(!r.isRing()) { 569 | continue; // sanity check, could probably be handled in a isSimple() check 570 | } 571 | 572 | if(area == 0d) { 573 | continue; // zero-area 574 | } 575 | 576 | // Outer ring winding order established by first polygon ring 577 | // If first ring (no outer) or next ring winding order matches outer ring winding order... 578 | if(outerPoly == null || (outerArea < 0 == area < 0)) { 579 | 580 | // Outer 581 | if(outerPoly != null) { 582 | polygons.add(geomFactory.createPolygon(outerPoly, holes.toArray(new LinearRing[holes.size()]))); 583 | holes.clear(); 584 | } 585 | 586 | outerPoly = r; 587 | outerArea = area; 588 | 589 | } else { 590 | 591 | // Hole 592 | if(Math.abs(outerArea) < Math.abs(area)) { 593 | continue; // Holes must have less area, could probably be handled in a isSimple() check 594 | } 595 | 596 | holes.add(r); 597 | } 598 | } 599 | 600 | if(outerPoly != null) { 601 | polygons.add(geomFactory.createPolygon(outerPoly, holes.toArray(new LinearRing[holes.size()]))); 602 | } 603 | 604 | return polygons; 605 | } 606 | } 607 | } -------------------------------------------------------------------------------- /src/main/java/com/wdtinc/mapbox_vector_tile/adapt/jts/JtsAdapter.java: -------------------------------------------------------------------------------- 1 | package com.wdtinc.mapbox_vector_tile.adapt.jts; 2 | 3 | import org.locationtech.jts.algorithm.Area; 4 | import org.locationtech.jts.geom.*; 5 | import org.locationtech.jts.geom.util.AffineTransformation; 6 | import org.locationtech.jts.simplify.TopologyPreservingSimplifier; 7 | import com.wdtinc.mapbox_vector_tile.*; 8 | import com.wdtinc.mapbox_vector_tile.build.MvtLayerProps; 9 | import com.wdtinc.mapbox_vector_tile.encoding.MvtUtil; 10 | import com.wdtinc.mapbox_vector_tile.build.MvtLayerParams; 11 | import com.wdtinc.mapbox_vector_tile.encoding.GeomCmdHdr; 12 | import com.wdtinc.mapbox_vector_tile.encoding.GeomCmd; 13 | import com.wdtinc.mapbox_vector_tile.encoding.ZigZag; 14 | import com.wdtinc.mapbox_vector_tile.util.Vec2d; 15 | import org.slf4j.LoggerFactory; 16 | 17 | import java.util.*; 18 | 19 | /** 20 | * Adapt JTS {@link Geometry} to 'Mapbox Vector Tile' objects. 21 | */ 22 | public final class JtsAdapter { 23 | 24 | /** 25 | * Create geometry clipped and then converted to MVT 'extent' coordinates. Result 26 | * contains both clipped geometry (intersection) and transformed geometry for encoding to MVT. 27 | * 28 | *

Uses the same tile and clipping coordinates. May cause rendering issues on boundaries for polygons 29 | * or line geometry depending on styling.

30 | * 31 | * @param g original 'source' geometry 32 | * @param tileEnvelope world coordinate bounds for tile 33 | * @param geomFactory creates a geometry for the tile envelope 34 | * @param mvtLayerParams specifies vector tile properties 35 | * @param filter geometry values that fail filter after transforms are removed 36 | * @return tile geometry result 37 | * @see TileGeomResult 38 | */ 39 | public static TileGeomResult createTileGeom(Geometry g, 40 | Envelope tileEnvelope, 41 | GeometryFactory geomFactory, 42 | MvtLayerParams mvtLayerParams, 43 | IGeometryFilter filter) { 44 | return createTileGeom(flatFeatureList(g), tileEnvelope, geomFactory, 45 | mvtLayerParams, filter); 46 | } 47 | 48 | /** 49 | *

Create geometry clipped and then converted to MVT 'extent' coordinates. Result 50 | * contains both clipped geometry (intersection) and transformed geometry for encoding to MVT.

51 | * 52 | *

Uses the same tile and clipping coordinates. May cause rendering issues on boundaries for polygons 53 | * or line geometry depending on styling.

54 | * 55 | * @param g original 'source' geometry, passed through {@link #flatFeatureList(Geometry)} 56 | * @param tileEnvelope world coordinate bounds for tile 57 | * @param geomFactory creates a geometry for the tile envelope 58 | * @param mvtLayerParams specifies vector tile properties 59 | * @param filter geometry values that fail filter after transforms are removed 60 | * @return tile geometry result 61 | * @see TileGeomResult 62 | */ 63 | public static TileGeomResult createTileGeom(List g, 64 | Envelope tileEnvelope, 65 | GeometryFactory geomFactory, 66 | MvtLayerParams mvtLayerParams, 67 | IGeometryFilter filter) { 68 | return createTileGeom(g, tileEnvelope, tileEnvelope, geomFactory, mvtLayerParams, filter); 69 | } 70 | 71 | /** 72 | *

Create geometry clipped and then converted to MVT 'extent' coordinates. Result 73 | * contains both clipped geometry (intersection) and transformed geometry for encoding to MVT.

74 | * 75 | *

Allows specifying separate tile and clipping coordinates. {@code clipEnvelope} can be bigger than 76 | * {@code tileEnvelope} to have geometry exist outside the MVT tile extent.

77 | * 78 | * @param g original 'source' geometry, passed through {@link #flatFeatureList(Geometry)} 79 | * @param tileEnvelope world coordinate bounds for tile, used for transforms 80 | * @param clipEnvelope world coordinates to clip tile by 81 | * @param geomFactory creates a geometry for the tile envelope 82 | * @param mvtLayerParams specifies vector tile properties 83 | * @param filter geometry values that fail filter after transforms are removed 84 | * @return tile geometry result 85 | * @see TileGeomResult 86 | */ 87 | public static TileGeomResult createTileGeom(List g, 88 | Envelope tileEnvelope, 89 | Envelope clipEnvelope, 90 | GeometryFactory geomFactory, 91 | MvtLayerParams mvtLayerParams, 92 | IGeometryFilter filter) { 93 | 94 | final Geometry tileClipGeom = geomFactory.toGeometry(clipEnvelope); 95 | 96 | final AffineTransformation t = new AffineTransformation(); 97 | final double xDiff = tileEnvelope.getWidth(); 98 | final double yDiff = tileEnvelope.getHeight(); 99 | 100 | final double xOffset = -tileEnvelope.getMinX(); 101 | final double yOffset = -tileEnvelope.getMinY(); 102 | 103 | // Transform Setup: Shift to 0 as minimum value 104 | t.translate(xOffset, yOffset); 105 | 106 | // Transform Setup: Scale X and Y to tile extent values, flip Y values 107 | t.scale(1d / (xDiff / (double) mvtLayerParams.extent), 108 | -1d / (yDiff / (double) mvtLayerParams.extent)); 109 | 110 | // Transform Setup: Bump Y values to positive quadrant 111 | t.translate(0d, (double) mvtLayerParams.extent); 112 | 113 | 114 | // The area contained in BOTH the 'original geometry', g, AND the 'clip envelope geometry' is the 'tile geometry' 115 | final List intersectedGeoms = flatIntersection(tileClipGeom, g); 116 | final List transformedGeoms = new ArrayList<>(intersectedGeoms.size()); 117 | 118 | // Transform intersected geometry 119 | Geometry nextTransformGeom; 120 | Object nextUserData; 121 | for(Geometry nextInterGeom : intersectedGeoms) { 122 | nextUserData = nextInterGeom.getUserData(); 123 | 124 | nextTransformGeom = t.transform(nextInterGeom); 125 | 126 | // Floating --> Integer, still contained within doubles 127 | nextTransformGeom.apply(RoundingFilter.INSTANCE); 128 | 129 | // TODO: Refactor line simplification 130 | nextTransformGeom = TopologyPreservingSimplifier.simplify(nextTransformGeom, .1d); // Can't use 0d, specify value < .5d 131 | 132 | nextTransformGeom.setUserData(nextUserData); 133 | 134 | // Apply filter on transformed geometry 135 | if(filter.accept(nextTransformGeom)) { 136 | transformedGeoms.add(nextTransformGeom); 137 | } 138 | } 139 | 140 | return new TileGeomResult(intersectedGeoms, transformedGeoms); 141 | } 142 | 143 | /** 144 | * JTS 1.14 does not support intersection on a {@link GeometryCollection}. This function works around this 145 | * by performing intersection on a flat list of geometry. The resulting list is pre-filtered for invalid 146 | * or empty geometry (outside of bounds). Invalid geometry are logged as errors. 147 | * 148 | * @param envelope non-list geometry defines bounding area 149 | * @param dataGeoms geometry pre-passed through {@link #flatFeatureList(Geometry)} 150 | * @return list of geometry from {@code data} intersecting with {@code envelope}. 151 | */ 152 | private static List flatIntersection(Geometry envelope, List dataGeoms) { 153 | final List intersectedGeoms = new ArrayList<>(dataGeoms.size()); 154 | 155 | Geometry nextIntersected; 156 | for(Geometry nextGeom : dataGeoms) { 157 | try { 158 | 159 | // AABB intersection culling 160 | if(envelope.getEnvelopeInternal().intersects(nextGeom.getEnvelopeInternal())) { 161 | 162 | nextIntersected = envelope.intersection(nextGeom); 163 | if(!nextIntersected.isEmpty()) { 164 | nextIntersected.setUserData(nextGeom.getUserData()); 165 | intersectedGeoms.add(nextIntersected); 166 | } 167 | } 168 | 169 | } catch (TopologyException e) { 170 | LoggerFactory.getLogger(JtsAdapter.class).error(e.getMessage(), e); 171 | } 172 | } 173 | 174 | return intersectedGeoms; 175 | } 176 | 177 | /** 178 | * Get the MVT type mapping for the provided JTS Geometry. 179 | * 180 | * @param geometry JTS Geometry to get MVT type for 181 | * @return MVT type for the given JTS Geometry, may return 182 | * {@link com.wdtinc.mapbox_vector_tile.VectorTile.Tile.GeomType#UNKNOWN} 183 | */ 184 | public static VectorTile.Tile.GeomType toGeomType(Geometry geometry) { 185 | VectorTile.Tile.GeomType result = VectorTile.Tile.GeomType.UNKNOWN; 186 | 187 | if(geometry instanceof Point 188 | || geometry instanceof MultiPoint) { 189 | result = VectorTile.Tile.GeomType.POINT; 190 | 191 | } else if(geometry instanceof LineString 192 | || geometry instanceof MultiLineString) { 193 | result = VectorTile.Tile.GeomType.LINESTRING; 194 | 195 | } else if(geometry instanceof Polygon 196 | || geometry instanceof MultiPolygon) { 197 | result = VectorTile.Tile.GeomType.POLYGON; 198 | } 199 | 200 | return result; 201 | } 202 | 203 | /** 204 | *

Recursively convert a {@link Geometry}, which may be an instance of {@link GeometryCollection} with mixed 205 | * element types, into a flat list containing only the following {@link Geometry} types:

206 | *
    207 | *
  • {@link Point}
  • 208 | *
  • {@link LineString}
  • 209 | *
  • {@link Polygon}
  • 210 | *
  • {@link MultiPoint}
  • 211 | *
  • {@link MultiLineString}
  • 212 | *
  • {@link MultiPolygon}
  • 213 | *
214 | *

WARNING: Any other Geometry types that were not mentioned in the list above will be discarded!

215 | *

Useful for converting a generic geometry into a list of simple MVT-feature-ready geometries.

216 | * 217 | * @param geom geometry to flatten 218 | * @return list of MVT-feature-ready geometries 219 | */ 220 | public static List flatFeatureList(Geometry geom) { 221 | final List singleGeoms = new ArrayList<>(); 222 | final Stack geomStack = new Stack<>(); 223 | 224 | Geometry nextGeom; 225 | int nextGeomCount; 226 | 227 | geomStack.push(geom); 228 | while(!geomStack.isEmpty()) { 229 | nextGeom = geomStack.pop(); 230 | 231 | if(nextGeom instanceof Point 232 | || nextGeom instanceof MultiPoint 233 | || nextGeom instanceof LineString 234 | || nextGeom instanceof MultiLineString 235 | || nextGeom instanceof Polygon 236 | || nextGeom instanceof MultiPolygon) { 237 | 238 | singleGeoms.add(nextGeom); 239 | 240 | } else if(nextGeom instanceof GeometryCollection) { 241 | 242 | // Push all child geometries 243 | nextGeomCount = nextGeom.getNumGeometries(); 244 | for(int i = 0; i < nextGeomCount; ++i) { 245 | geomStack.push(nextGeom.getGeometryN(i)); 246 | } 247 | 248 | } 249 | } 250 | 251 | return singleGeoms; 252 | } 253 | 254 | /** 255 | *

Convert JTS {@link Geometry} to a list of vector tile features. 256 | * The Geometry should be in MVT coordinates.

257 | * 258 | *

Each geometry will have its own ID.

259 | * 260 | * @param geometry JTS geometry to convert 261 | * @param layerProps layer properties for tagging features 262 | * @param userDataConverter convert {@link Geometry#getUserData()} to MVT feature tags 263 | * @see #flatFeatureList(Geometry) 264 | * @see #createTileGeom(Geometry, Envelope, GeometryFactory, MvtLayerParams, IGeometryFilter) 265 | */ 266 | public static List toFeatures(Geometry geometry, 267 | MvtLayerProps layerProps, 268 | IUserDataConverter userDataConverter) { 269 | return toFeatures(flatFeatureList(geometry), layerProps, userDataConverter); 270 | } 271 | 272 | /** 273 | *

Convert a flat list of JTS {@link Geometry} to a list of vector tile features. 274 | * The Geometry should be in MVT coordinates.

275 | * 276 | *

Each geometry will have its own ID.

277 | * 278 | * @param flatGeoms flat list of JTS geometry (in MVT coordinates) to convert 279 | * @param layerProps layer properties for tagging features 280 | * @param userDataConverter convert {@link Geometry#getUserData()} to MVT feature tags 281 | * @see #flatFeatureList(Geometry) 282 | * @see #createTileGeom(Geometry, Envelope, GeometryFactory, MvtLayerParams, IGeometryFilter) 283 | */ 284 | public static List toFeatures(Collection flatGeoms, 285 | MvtLayerProps layerProps, 286 | IUserDataConverter userDataConverter) { 287 | 288 | // Guard: empty geometry 289 | if(flatGeoms.isEmpty()) { 290 | return Collections.emptyList(); 291 | } 292 | 293 | final List features = new ArrayList<>(); 294 | final Vec2d cursor = new Vec2d(); 295 | 296 | VectorTile.Tile.Feature nextFeature; 297 | 298 | for(Geometry nextGeom : flatGeoms) { 299 | cursor.set(0d, 0d); 300 | nextFeature = toFeature(nextGeom, cursor, layerProps, userDataConverter); 301 | if(nextFeature != null) { 302 | features.add(nextFeature); 303 | } 304 | } 305 | 306 | return features; 307 | } 308 | 309 | /** 310 | * Create and return a feature from a geometry. Returns null on failure. 311 | * 312 | * @param geom flat geometry (in MVT coordinates) via {@link #flatFeatureList(Geometry)} that can be translated to a feature 313 | * @param cursor vector tile cursor position 314 | * @param layerProps layer properties for tagging features 315 | * @return new tile feature instance, or null on failure 316 | */ 317 | private static VectorTile.Tile.Feature toFeature(Geometry geom, 318 | Vec2d cursor, 319 | MvtLayerProps layerProps, 320 | IUserDataConverter userDataConverter) { 321 | 322 | // Guard: UNKNOWN Geometry 323 | final VectorTile.Tile.GeomType mvtGeomType = JtsAdapter.toGeomType(geom); 324 | if(mvtGeomType == VectorTile.Tile.GeomType.UNKNOWN) { 325 | return null; 326 | } 327 | 328 | 329 | final VectorTile.Tile.Feature.Builder featureBuilder = VectorTile.Tile.Feature.newBuilder(); 330 | final boolean mvtClosePath = MvtUtil.shouldClosePath(mvtGeomType); 331 | final List mvtGeom = new ArrayList<>(); 332 | 333 | featureBuilder.setType(mvtGeomType); 334 | 335 | if(geom instanceof Point || geom instanceof MultiPoint) { 336 | 337 | // Encode as MVT point or multipoint 338 | mvtGeom.addAll(ptsToGeomCmds(geom, cursor)); 339 | 340 | } else if(geom instanceof LineString || geom instanceof MultiLineString) { 341 | 342 | // Encode as MVT linestring or multi-linestring 343 | for (int i = 0; i < geom.getNumGeometries(); ++i) { 344 | mvtGeom.addAll(linesToGeomCmds(geom.getGeometryN(i), mvtClosePath, cursor, 1)); 345 | } 346 | 347 | } else if(geom instanceof MultiPolygon || geom instanceof Polygon) { 348 | 349 | // Encode as MVT polygon or multi-polygon 350 | for(int i = 0; i < geom.getNumGeometries(); ++i) { 351 | 352 | final Polygon nextPoly = (Polygon) geom.getGeometryN(i); 353 | final List nextPolyGeom = new ArrayList<>(); 354 | boolean valid = true; 355 | 356 | // Add exterior ring 357 | final LineString exteriorRing = nextPoly.getExteriorRing(); 358 | 359 | // Area must be non-zero 360 | final double exteriorArea = Area.ofRingSigned(exteriorRing.getCoordinates()); 361 | if(((int) Math.round(exteriorArea)) == 0) { 362 | continue; 363 | } 364 | 365 | // Check CCW Winding (must be positive area in original coordinate system, MVT is positive-y-down, so inequality is flipped) 366 | // See: https://docs.mapbox.com/vector-tiles/specification/#winding-order 367 | if(exteriorArea > 0d) { 368 | CoordinateArrays.reverse(exteriorRing.getCoordinates()); 369 | } 370 | 371 | nextPolyGeom.addAll(linesToGeomCmds(exteriorRing, mvtClosePath, cursor, 2)); 372 | 373 | 374 | // Add interior rings 375 | for(int ringIndex = 0; ringIndex < nextPoly.getNumInteriorRing(); ++ringIndex) { 376 | 377 | final LineString nextInteriorRing = nextPoly.getInteriorRingN(ringIndex); 378 | 379 | // Area must be non-zero 380 | final double interiorArea = Area.ofRingSigned(nextInteriorRing.getCoordinates()); 381 | if(((int)Math.round(interiorArea)) == 0) { 382 | continue; 383 | } 384 | 385 | // Check CW Winding (must be negative area in original coordinate system, MVT is positive-y-down, so inequality is flipped) 386 | // See: https://docs.mapbox.com/vector-tiles/specification/#winding-order 387 | if(interiorArea < 0d) { 388 | CoordinateArrays.reverse(nextInteriorRing.getCoordinates()); 389 | } 390 | 391 | // Interior ring area must be < exterior ring area, or entire geometry is invalid 392 | if(Math.abs(exteriorArea) <= Math.abs(interiorArea)) { 393 | valid = false; 394 | break; 395 | } 396 | 397 | nextPolyGeom.addAll(linesToGeomCmds(nextInteriorRing, mvtClosePath, cursor, 2)); 398 | } 399 | 400 | 401 | if(valid) { 402 | mvtGeom.addAll(nextPolyGeom); 403 | } 404 | } 405 | } 406 | 407 | 408 | if(mvtGeom.size() < 1) { 409 | return null; 410 | } 411 | 412 | featureBuilder.addAllGeometry(mvtGeom); 413 | 414 | 415 | // Feature Properties 416 | userDataConverter.addTags(geom.getUserData(), layerProps, featureBuilder); 417 | 418 | return featureBuilder.build(); 419 | } 420 | 421 | /** 422 | *

Convert a {@link Point} or {@link MultiPoint} geometry to a list of MVT geometry drawing commands. See 423 | * vector-tile-spec 424 | * for details.

425 | * 426 | *

WARNING: The value of the {@code cursor} parameter is modified as a result of calling this method.

427 | * 428 | * @param geom input of type {@link Point} or {@link MultiPoint}. Type is NOT checked and expected to be correct. 429 | * @param cursor modified during processing to contain next MVT cursor position 430 | * @return list of commands 431 | */ 432 | private static List ptsToGeomCmds(final Geometry geom, final Vec2d cursor) { 433 | 434 | // Guard: empty geometry coordinates 435 | final Coordinate[] geomCoords = geom.getCoordinates(); 436 | if(geomCoords.length <= 0) { 437 | return Collections.emptyList(); 438 | } 439 | 440 | /** Tile commands and parameters */ 441 | final List geomCmds = new ArrayList<>(geomCmdBuffLenPts(geomCoords.length)); 442 | 443 | /** Holds next MVT coordinate */ 444 | final Vec2d mvtPos = new Vec2d(); 445 | 446 | /** Length of 'MoveTo' draw command */ 447 | int moveCmdLen = 0; 448 | 449 | // Insert placeholder for 'MoveTo' command header 450 | geomCmds.add(0); 451 | 452 | Coordinate nextCoord; 453 | 454 | for(int i = 0; i < geomCoords.length; ++i) { 455 | nextCoord = geomCoords[i]; 456 | mvtPos.set(nextCoord.x, nextCoord.y); 457 | 458 | // Ignore duplicate MVT points 459 | if(i == 0 || !equalAsInts(cursor, mvtPos)) { 460 | ++moveCmdLen; 461 | moveCursor(cursor, geomCmds, mvtPos); 462 | } 463 | } 464 | 465 | 466 | if(moveCmdLen <= GeomCmdHdr.CMD_HDR_LEN_MAX) { 467 | 468 | // Write 'MoveTo' command header to first index 469 | geomCmds.set(0, GeomCmdHdr.cmdHdr(GeomCmd.MoveTo, moveCmdLen)); 470 | 471 | return geomCmds; 472 | 473 | } else { 474 | 475 | // Invalid geometry, need at least 1 'MoveTo' value to make points 476 | return Collections.emptyList(); 477 | } 478 | } 479 | 480 | /** 481 | *

Convert a {@link LineString} or {@link Polygon} to a list of MVT geometry drawing commands. 482 | * A {@link MultiLineString} or {@link MultiPolygon} can be encoded by calling this method multiple times.

483 | * 484 | *

See vector-tile-spec for details.

485 | * 486 | *

WARNING: The value of the {@code cursor} parameter is modified as a result of calling this method.

487 | * 488 | * @param geom input of type {@link LineString} or {@link Polygon}. Type is NOT checked and expected to be correct. 489 | * @param closeEnabled whether a 'ClosePath' command should terminate the command list 490 | * @param cursor modified during processing to contain next MVT cursor position 491 | * @param minLineToLen minimum allowed length for LineTo command. 492 | * @return list of commands 493 | */ 494 | private static List linesToGeomCmds( 495 | final Geometry geom, 496 | final boolean closeEnabled, 497 | final Vec2d cursor, 498 | final int minLineToLen) { 499 | 500 | final Coordinate[] geomCoords = geom.getCoordinates(); 501 | 502 | // Calculate the geometry coordinate count for processing that supports ignoring repeated final points 503 | final int geomProcCoordCount; 504 | if(closeEnabled) { 505 | 506 | // Check geometry for repeated end points when closing (Polygon rings) 507 | final int repeatEndCoordCount = countCoordRepeatReverse(geomCoords); 508 | geomProcCoordCount = geomCoords.length - repeatEndCoordCount; 509 | 510 | } else { 511 | 512 | // No closing (Line strings) 513 | geomProcCoordCount = geomCoords.length; 514 | } 515 | 516 | 517 | // Guard/Optimization: Not enough geometry coordinates for a line 518 | if(geomProcCoordCount < 2) { 519 | return Collections.emptyList(); 520 | } 521 | 522 | // Save cursor position if failure creating geometry occurs 523 | final Vec2d origCursorPos = new Vec2d(cursor); 524 | 525 | /** Tile commands and parameters */ 526 | final List geomCmds = new ArrayList<>(geomCmdBuffLenLines(geomProcCoordCount, closeEnabled)); 527 | 528 | /** Holds next MVT coordinate */ 529 | final Vec2d mvtPos = new Vec2d(); 530 | 531 | // Initial coordinate 532 | Coordinate nextCoord = geomCoords[0]; 533 | mvtPos.set(nextCoord.x, nextCoord.y); 534 | 535 | // Encode initial 'MoveTo' command 536 | geomCmds.add(GeomCmdHdr.cmdHdr(GeomCmd.MoveTo, 1)); 537 | 538 | moveCursor(cursor, geomCmds, mvtPos); 539 | 540 | 541 | /** Index of 'LineTo' 'command header' */ 542 | final int lineToCmdHdrIndex = geomCmds.size(); 543 | 544 | // Insert placeholder for 'LineTo' command header 545 | geomCmds.add(0); 546 | 547 | 548 | /** Length of 'LineTo' draw command */ 549 | int lineToLength = 0; 550 | 551 | for(int i = 1; i < geomProcCoordCount; ++i) { 552 | nextCoord = geomCoords[i]; 553 | mvtPos.set(nextCoord.x, nextCoord.y); 554 | 555 | // Ignore duplicate MVT points in sequence 556 | if(!equalAsInts(cursor, mvtPos)) { 557 | ++lineToLength; 558 | moveCursor(cursor, geomCmds, mvtPos); 559 | } 560 | } 561 | 562 | if(lineToLength >= minLineToLen && lineToLength <= GeomCmdHdr.CMD_HDR_LEN_MAX) { 563 | 564 | // Write 'LineTo' 'command header' 565 | geomCmds.set(lineToCmdHdrIndex, GeomCmdHdr.cmdHdr(GeomCmd.LineTo, lineToLength)); 566 | 567 | if(closeEnabled) { 568 | geomCmds.add(GeomCmdHdr.closePathCmdHdr()); 569 | } 570 | 571 | return geomCmds; 572 | 573 | } else { 574 | 575 | // Revert cursor position 576 | cursor.set(origCursorPos); 577 | 578 | // Invalid geometry, need at least 1 'LineTo' value to make a Multiline or Polygon 579 | return Collections.emptyList(); 580 | } 581 | } 582 | 583 | /** 584 | *

Count number of coordinates starting from the end of the coordinate array backwards 585 | * that match the first coordinate value.

586 | * 587 | *

Useful for ensuring self-closing line strings do not repeat the first coordinate.

588 | * 589 | * @param coords coordinates to check for duplicate points 590 | * @return number of duplicate points at the rear of the list 591 | */ 592 | private static int countCoordRepeatReverse(Coordinate[] coords) { 593 | int repeatCoords = 0; 594 | 595 | final Coordinate firstCoord = coords[0]; 596 | Coordinate nextCoord; 597 | 598 | for(int i = coords.length - 1; i > 0; --i) { 599 | nextCoord = coords[i]; 600 | if(equalAsInts2d(firstCoord, nextCoord)) { 601 | ++repeatCoords; 602 | } else { 603 | break; 604 | } 605 | } 606 | 607 | return repeatCoords; 608 | } 609 | 610 | /** 611 | *

Appends {@link ZigZag#encode(int)} of delta in x,y from {@code cursor} to {@code mvtPos} into the {@code geomCmds} buffer.

612 | * 613 | *

Afterwards, the {@code cursor} values are changed to match the {@code mvtPos} values.

614 | * 615 | * @param cursor MVT cursor position 616 | * @param geomCmds geometry command list 617 | * @param mvtPos next MVT cursor position 618 | */ 619 | private static void moveCursor(Vec2d cursor, List geomCmds, Vec2d mvtPos) { 620 | 621 | // Delta, then zigzag 622 | geomCmds.add(ZigZag.encode((int)mvtPos.x - (int)cursor.x)); 623 | geomCmds.add(ZigZag.encode((int)mvtPos.y - (int)cursor.y)); 624 | 625 | cursor.set(mvtPos); 626 | } 627 | 628 | /** 629 | * Return true if the values of the two {@link Coordinate} are equal when their 630 | * first and second ordinates are cast as ints. Ignores 3rd ordinate. 631 | * 632 | * @param a first coordinate to compare 633 | * @param b second coordinate to compare 634 | * @return true if the values of the two {@link Coordinate} are equal when their 635 | * first and second ordinates are cast as ints 636 | */ 637 | private static boolean equalAsInts2d(Coordinate a, Coordinate b) { 638 | return ((int)a.getOrdinate(0)) == ((int)b.getOrdinate(0)) 639 | && ((int)a.getOrdinate(1)) == ((int)b.getOrdinate(1)); 640 | } 641 | 642 | /** 643 | * Return true if the values of the two vectors are equal when cast as ints. 644 | * 645 | * @param a first vector to compare 646 | * @param b second vector to compare 647 | * @return true if the values of the two vectors are equal when cast as ints 648 | */ 649 | private static boolean equalAsInts(Vec2d a, Vec2d b) { 650 | return ((int) a.x) == ((int) b.x) && ((int) a.y) == ((int) b.y); 651 | } 652 | 653 | /** 654 | * Get required geometry buffer size for a {@link Point} or {@link MultiPoint} geometry. 655 | * 656 | * @param coordCount coordinate count for the geometry 657 | * @return required geometry buffer length 658 | */ 659 | private static int geomCmdBuffLenPts(int coordCount) { 660 | 661 | // 1 MoveTo Header, 2 parameters * coordCount 662 | return 1 + (coordCount * 2); 663 | } 664 | 665 | /** 666 | * Get required geometry buffer size for a {@link LineString} or {@link Polygon} geometry. 667 | * 668 | * @param coordCount coordinate count for the geometry 669 | * @param closeEnabled whether a 'ClosePath' command should terminate the command list 670 | * @return required geometry buffer length 671 | */ 672 | private static int geomCmdBuffLenLines(int coordCount, boolean closeEnabled) { 673 | 674 | // MoveTo Header, LineTo Header, Optional ClosePath Header, 2 parameters * coordCount 675 | return 2 + (closeEnabled ? 1 : 0) + (coordCount * 2); 676 | } 677 | } 678 | --------------------------------------------------------------------------------