You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@baremaps.apache.org by bc...@apache.org on 2023/03/29 13:19:55 UTC
[incubator-baremaps] 01/01: Implement the vector tile specification
This is an automated email from the ASF dual-hosted git repository.
bchapuis pushed a commit to branch 616-vectortile
in repository https://gitbox.apache.org/repos/asf/incubator-baremaps.git
commit dd6def3a58080c2571a4b60e095fdf98a011d39d
Author: Bertil Chapuis <bc...@gmail.com>
AuthorDate: Sun Mar 26 12:29:12 2023 +0200
Implement the vector tile specification
---
.../org/apache/baremaps/vectortile/Feature.java | 118 ++++++
.../java/org/apache/baremaps/vectortile/Layer.java | 119 ++++++
.../java/org/apache/baremaps/vectortile/Tile.java | 64 ++++
.../baremaps/vectortile/VectorTileDecoder.java | 386 ++++++++++++++++++++
.../baremaps/vectortile/VectorTileEncoder.java | 361 ++++++++++++++++++
.../baremaps/vectortile/VectorTileUtils.java | 116 ++++++
baremaps-core/src/main/proto/vector_tile.proto | 402 +++++++++++++++++++++
.../baremaps/vectortile/VectorTileDecoderTest.java | 170 +++++++++
.../baremaps/vectortile/VectorTileEncoderTest.java | 178 +++++++++
.../apache/baremaps/vectortile/VectorTileTest.java | 42 +++
.../baremaps/vectortile/VectorTileUtilsTest.java | 64 ++++
11 files changed, 2020 insertions(+)
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/vectortile/Feature.java b/baremaps-core/src/main/java/org/apache/baremaps/vectortile/Feature.java
new file mode 100644
index 00000000..c9d7dccb
--- /dev/null
+++ b/baremaps-core/src/main/java/org/apache/baremaps/vectortile/Feature.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.baremaps.vectortile;
+
+import com.google.common.base.Objects;
+import java.util.Map;
+import org.locationtech.jts.geom.Geometry;
+
+/**
+ * A vector tile layer.
+ */
+public class Feature {
+
+ private long id;
+
+ private Map<String, Object> tags;
+
+ private Geometry geometry;
+
+ /**
+ * Creates a new feature.
+ */
+ public Feature() {}
+
+ /**
+ * Creates a new feature.
+ *
+ * @param id The id of the feature.
+ * @param tags The tags of the feature.
+ * @param geometry The geometry of the feature.
+ */
+ public Feature(long id, Map<String, Object> tags, Geometry geometry) {
+ this.id = id;
+ this.tags = tags;
+ this.geometry = geometry;
+ }
+
+ /**
+ * Returns the id of the feature.
+ *
+ * @return The id of the feature.
+ */
+ public long getId() {
+ return id;
+ }
+
+ /**
+ * Sets the id of the feature.
+ *
+ * @param id The id of the feature.
+ */
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ /**
+ * Returns the tags of the feature.
+ *
+ * @return The tags of the feature.
+ */
+ public Map<String, Object> getTags() {
+ return tags;
+ }
+
+ /**
+ * Sets the tags of the feature.
+ *
+ * @param tags The tags of the feature.
+ */
+ public void setTags(Map<String, Object> tags) {
+ this.tags = tags;
+ }
+
+ /**
+ * Returns the geometry of the feature.
+ *
+ * @return The geometry of the feature.
+ */
+ public Geometry getGeometry() {
+ return geometry;
+ }
+
+ /**
+ * Sets the geometry of the feature.
+ *
+ * @param geometry The geometry of the feature.
+ */
+ public void setGeometry(Geometry geometry) {
+ this.geometry = geometry;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ Feature feature = (Feature) o;
+ return id == feature.id
+ && Objects.equal(tags, feature.tags)
+ && Objects.equal(geometry, feature.geometry);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(id, tags, geometry);
+ }
+}
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/vectortile/Layer.java b/baremaps-core/src/main/java/org/apache/baremaps/vectortile/Layer.java
new file mode 100644
index 00000000..0393f2ca
--- /dev/null
+++ b/baremaps-core/src/main/java/org/apache/baremaps/vectortile/Layer.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.baremaps.vectortile;
+
+import com.google.common.base.Objects;
+import java.util.List;
+
+/**
+ * A vector tile layer.
+ */
+public class Layer {
+
+ private String name;
+
+ private int extent;
+
+ private List<Feature> features;
+
+ /**
+ * Creates a new layer.
+ */
+ public Layer() {
+
+ }
+
+ /**
+ * Creates a new layer.
+ *
+ * @param name The name of the layer.
+ * @param extent The extent of the layer.
+ * @param features The features of the layer.
+ */
+ public Layer(String name, int extent, List<Feature> features) {
+ this.name = name;
+ this.extent = extent;
+ this.features = features;
+ }
+
+ /**
+ * Returns the name of the layer.
+ *
+ * @return The name of the layer.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the name of the layer.
+ *
+ * @param name The name of the layer.
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Returns the extent of the layer.
+ *
+ * @return The extent of the layer.
+ */
+ public int getExtent() {
+ return extent;
+ }
+
+ /**
+ * Sets the extent of the layer.
+ *
+ * @param extent The extent of the layer.
+ */
+ public void setExtent(int extent) {
+ this.extent = extent;
+ }
+
+ /**
+ * Returns the features of the layer.
+ *
+ * @return The features of the layer.
+ */
+ public List<Feature> getFeatures() {
+ return features;
+ }
+
+ /**
+ * Sets the features of the layer.
+ *
+ * @param features The features of the layer.
+ */
+ public void setFeatures(List<Feature> features) {
+ this.features = features;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ Layer layer = (Layer) o;
+ return extent == layer.extent
+ && Objects.equal(name, layer.name)
+ && Objects.equal(features, layer.features);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(name, extent, features);
+ }
+}
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/vectortile/Tile.java b/baremaps-core/src/main/java/org/apache/baremaps/vectortile/Tile.java
new file mode 100644
index 00000000..7fb6b7c4
--- /dev/null
+++ b/baremaps-core/src/main/java/org/apache/baremaps/vectortile/Tile.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.baremaps.vectortile;
+
+import com.google.common.base.Objects;
+import java.util.List;
+
+/**
+ * A vector tile layer.
+ */
+public class Tile {
+
+ private List<Layer> layers;
+
+ /**
+ * Creates a new tile.
+ */
+ public Tile(List<Layer> layers) {
+ this.layers = layers;
+ }
+
+ /**
+ * Returns the layers of the tile.
+ *
+ * @return The layers of the tile.
+ */
+ public List<Layer> getLayers() {
+ return layers;
+ }
+
+ /**
+ * Sets the layers of the tile.
+ *
+ * @param layers The layers of the tile.
+ */
+ public void setLayers(List<Layer> layers) {
+ this.layers = layers;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ Tile tile = (Tile) o;
+ return Objects.equal(layers, tile.layers);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(layers);
+ }
+}
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/vectortile/VectorTileDecoder.java b/baremaps-core/src/main/java/org/apache/baremaps/vectortile/VectorTileDecoder.java
new file mode 100644
index 00000000..61d00976
--- /dev/null
+++ b/baremaps-core/src/main/java/org/apache/baremaps/vectortile/VectorTileDecoder.java
@@ -0,0 +1,386 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.baremaps.vectortile;
+
+import static org.apache.baremaps.vectortile.VectorTileUtils.*;
+
+import java.util.*;
+import java.util.stream.Collectors;
+import org.apache.baremaps.mvt.binary.VectorTile;
+import org.apache.baremaps.mvt.binary.VectorTile.Tile.GeomType;
+import org.apache.baremaps.mvt.binary.VectorTile.Tile.Value;
+import org.locationtech.jts.geom.*;
+
+/**
+ * A vector tile decoder.
+ *
+ * This implementation is based on the Vector Tile Specification 2.1.
+ */
+public class VectorTileDecoder {
+ private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();
+
+ private int cx = 0;
+
+ private int cy = 0;
+
+ private List<String> keys = new ArrayList<>();
+
+ private List<Object> values = new ArrayList<>();
+
+ /**
+ * Constructs a new vector tile decoder.
+ */
+ public VectorTileDecoder() {}
+
+ /**
+ * Decodes a vector tile.
+ *
+ * @param tile The vector tile to decode.
+ * @return The decoded vector tile.
+ */
+ public Tile decodeTile(VectorTile.Tile tile) {
+ List<Layer> layers = tile.getLayersList().stream()
+ .map(this::decodeLayer)
+ .collect(Collectors.toList());
+ return new Tile(layers);
+ }
+
+ /**
+ * Decodes a vector tile layer.
+ *
+ * @param layer The vector tile layer.
+ * @return The decoded layer.
+ */
+ public Layer decodeLayer(VectorTile.Tile.Layer layer) {
+ String name = layer.getName();
+ int extent = layer.getExtent();
+
+ cx = 0;
+ cy = 0;
+
+ keys = layer.getKeysList();
+ values = layer.getValuesList().stream()
+ .map(this::decodeValue)
+ .collect(Collectors.toList());
+
+ List<Feature> features = layer.getFeaturesList().stream()
+ .map(this::decodeFeature)
+ .collect(Collectors.toList());
+
+ return new Layer(name, extent, features);
+ }
+
+ /**
+ * Decodes a vector tile value into a Java object.
+ *
+ * @param value The vector tile value.
+ * @return The Java object.
+ */
+ protected Object decodeValue(Value value) {
+ if (value.hasStringValue()) {
+ return value.getStringValue();
+ } else if (value.hasFloatValue()) {
+ return value.getFloatValue();
+ } else if (value.hasDoubleValue()) {
+ return value.getDoubleValue();
+ } else if (value.hasIntValue()) {
+ return value.getIntValue();
+ } else if (value.hasSintValue()) {
+ return value.getSintValue();
+ } else if (value.hasUintValue()) {
+ return value.getUintValue();
+ } else if (value.hasBoolValue()) {
+ return value.getBoolValue();
+ } else {
+ throw new IllegalStateException("Value is not set.");
+ }
+ }
+
+ /**
+ * Decodes a feature from a vector tile.
+ *
+ * @param feature The vector tile feature to decode
+ * @return The decoded feature
+ */
+ protected Feature decodeFeature(VectorTile.Tile.Feature feature) {
+ long id = feature.getId();
+ Map<String, Object> tags = decodeTags(feature);
+ Geometry geometry = decodeGeometry(feature);
+ return new Feature(id, tags, geometry);
+ }
+
+ /**
+ * Decodes the tags from a vector tile feature.
+ *
+ * @param feature The feature to decode
+ * @return The tags of the feature
+ */
+ protected Map<String, Object> decodeTags(VectorTile.Tile.Feature feature) {
+ Map<String, Object> tags = new HashMap<>();
+ List<Integer> encoding = feature.getTagsList();
+ for (int i = 0; i < encoding.size(); i += 2) {
+ int key = encoding.get(i);
+ int value = encoding.get(i + 1);
+ tags.put(keys.get(key), values.get(value));
+ }
+ return tags;
+ }
+
+ /**
+ * Decodes a geometry from a vector tile feature.
+ *
+ * @param feature The vector tile feature
+ * @return The decoded geometry
+ */
+ protected Geometry decodeGeometry(VectorTile.Tile.Feature feature) {
+ GeomType type = feature.getType();
+ List<Integer> encoding = feature.getGeometryList();
+ switch (type) {
+ case POINT:
+ return decodePoint(encoding);
+ case LINESTRING:
+ return decodeLineString(encoding);
+ case POLYGON:
+ return decodePolygon(encoding);
+ case UNKNOWN:
+ default:
+ throw new IllegalStateException("Unknown geometry type.");
+ }
+ }
+
+ /**
+ * Decode a point geometry.
+ *
+ * @param encoding The encoding of the point geometry
+ * @return The decoded point geometry
+ */
+ protected Geometry decodePoint(List<Integer> encoding) {
+ List<Coordinate> coordinates = new ArrayList<>();
+
+ // Iterate over the commands and parameters
+ int i = 0;
+ while (i < encoding.size()) {
+ int value = encoding.get(i);
+ int command = command(value);
+ int count = count(value);
+
+ // Increment the index to the first parameter
+ i++;
+
+ // Iterate over the parameters
+ int length = count * 2;
+ for (int j = 0; j < length; j += 2) {
+ // Decode the parameters and move the cursor
+ cx += parameter(encoding.get(i + j));
+ cy += parameter(encoding.get(i + j + 1));
+
+ // Add the coordinate to the list
+ if (command == MOVE_TO) {
+ coordinates.add(new Coordinate(cx, cy));
+ }
+ }
+
+ // Increment the index to the next command
+ i += length;
+ }
+
+ // Build the final geometry
+ if (coordinates.size() == 1) {
+ return GEOMETRY_FACTORY.createPoint(coordinates.get(0));
+ } else if (coordinates.size() > 1) {
+ return GEOMETRY_FACTORY.createMultiPointFromCoords(coordinates.toArray(new Coordinate[0]));
+ } else {
+ throw new IllegalStateException("No coordinates found.");
+ }
+ }
+
+ /**
+ * Decode a line string.
+ *
+ * @param encoding The encoding of the line string
+ * @return The decoded line string
+ */
+ protected Geometry decodeLineString(List<Integer> encoding) {
+ List<LineString> lineStrings = new ArrayList<>();
+ List<Coordinate> coordinates = new ArrayList<>();
+
+ // Iterate over the commands and parameters
+ int i = 0;
+ while (i < encoding.size()) {
+ int value = encoding.get(i);
+ int command = command(value);
+ int count = count(value);
+
+ // Increment the index to the first parameter
+ i++;
+
+ // Iterate over the parameters
+ int length = count * 2;
+ for (int j = 0; j < length; j += 2) {
+ // Decode the parameters and move the cursor
+ cx += parameter(encoding.get(i + j));
+ cy += parameter(encoding.get(i + j + 1));
+
+ // Start a new linestring
+ if (command == MOVE_TO) {
+ coordinates.clear();
+ coordinates.add(new Coordinate(cx, cy));
+ }
+
+ // Add the coordinate to the current linestring
+ else if (command == LINE_TO) {
+ coordinates.add(new Coordinate(cx, cy));
+ }
+ }
+
+ // Add the linestring to the list of linestrings
+ if (coordinates.size() > 1) {
+ lineStrings.add(GEOMETRY_FACTORY.createLineString(coordinates.toArray(new Coordinate[0])));
+ }
+
+ // Increment the index to the next command
+ i += length;
+ }
+
+ // Build the final geometry
+ if (lineStrings.size() == 1) {
+ return lineStrings.get(0);
+ } else if (lineStrings.size() > 1) {
+ return GEOMETRY_FACTORY.createMultiLineString(lineStrings.toArray(new LineString[0]));
+ } else {
+ throw new IllegalStateException("No linestrings found.");
+ }
+ }
+
+ /**
+ * Decodes a polygon geometry.
+ *
+ * @param encoding The encoding of the polygon
+ * @return The geometry
+ */
+ protected Geometry decodePolygon(List<Integer> encoding) {
+ List<Polygon> polygons = new ArrayList<>();
+ Optional<LinearRing> shell = Optional.empty();
+ List<LinearRing> holes = new ArrayList<>();
+ List<Coordinate> coordinates = new ArrayList<>();
+
+ // Iterate over the commands and parameters
+ int i = 0;
+ while (i < encoding.size()) {
+ int value = encoding.get(i);
+ int command = command(value);
+ int count = count(value);
+
+ // Accumulate the coordinates
+ if (command == MOVE_TO || command == LINE_TO) {
+
+ // Increment the index to the first parameter
+ i++;
+
+ int length = count * 2;
+ for (int j = 0; j < length; j += 2) {
+ // Decode the parameters and move the cursor
+ cx += parameter(encoding.get(i + j));
+ cy += parameter(encoding.get(i + j + 1));
+
+ // Start a new linear ring
+ if (command == MOVE_TO) {
+ coordinates.clear();
+ coordinates.add(new Coordinate(cx, cy));
+ }
+
+ // Add the coordinate to the current linear ring
+ else if (command == LINE_TO) {
+ coordinates.add(new Coordinate(cx, cy));
+ }
+ }
+
+ // Increment the index to the next command
+ i += length;
+ }
+
+ // Assemble the linear rings
+ if (command == CLOSE_PATH) {
+ coordinates.add(coordinates.get(0));
+ LinearRing linearRing =
+ GEOMETRY_FACTORY.createLinearRing(coordinates.toArray(new Coordinate[0]));
+ boolean isShell = isClockWise(linearRing);
+
+ // Build the previous polygon
+ if (isShell && shell.isPresent()) {
+ polygons
+ .add(GEOMETRY_FACTORY.createPolygon(shell.get(), holes.toArray(new LinearRing[0])));
+ holes.clear();
+ }
+
+ // Add the linear ring to the appropriate variable
+ if (isShell) {
+ shell = Optional.of(linearRing);
+ } else {
+ holes.add(linearRing);
+ }
+
+ // Reset the coordinates
+ coordinates.clear();
+
+ // Increment the index to the next command
+ i++;
+ }
+ }
+
+ // Build the last polygon
+ if (shell.isPresent()) {
+ polygons.add(GEOMETRY_FACTORY.createPolygon(shell.get(), holes.toArray(new LinearRing[0])));
+ holes.clear();
+ }
+
+ // Build the final geometry
+ if (polygons.size() == 1) {
+ return polygons.get(0);
+ } else if (polygons.size() > 1) {
+ return GEOMETRY_FACTORY.createMultiPolygon(polygons.toArray(new Polygon[0]));
+ } else {
+ throw new IllegalStateException("No polygons found.");
+ }
+ }
+
+ /**
+ * Returns the command for the given value.
+ *
+ * @param value The value
+ * @return The command
+ */
+ protected int command(int value) {
+ return value & 0x7;
+ }
+
+ /**
+ * Returns the number of parameters for the given value.
+ *
+ * @param value The value
+ * @return The number of parameters
+ */
+ protected int count(int value) {
+ return value >> 3;
+ }
+
+ /**
+ * Decodes a parameter from the given value.
+ *
+ * @param value The value to decode
+ * @return The decoded parameter
+ */
+ protected Integer parameter(int value) {
+ return (value >> 1) ^ (-(value & 1));
+ }
+}
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/vectortile/VectorTileEncoder.java b/baremaps-core/src/main/java/org/apache/baremaps/vectortile/VectorTileEncoder.java
new file mode 100644
index 00000000..b9f2fe61
--- /dev/null
+++ b/baremaps-core/src/main/java/org/apache/baremaps/vectortile/VectorTileEncoder.java
@@ -0,0 +1,361 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.baremaps.vectortile;
+
+import static org.apache.baremaps.vectortile.VectorTileUtils.*;
+
+import com.google.common.collect.Lists;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.function.Consumer;
+import org.apache.baremaps.mvt.binary.VectorTile;
+import org.locationtech.jts.geom.*;
+
+/**
+ * A vector tile encoder.
+ *
+ * This implementation is based on the Vector Tile Specification 2.1.
+ */
+public class VectorTileEncoder {
+
+ private int cx = 0;
+
+ private int cy = 0;
+
+ private List<String> keys = new ArrayList<>();
+
+ private List<Object> values = new ArrayList<>();
+
+ /**
+ * Constructs a vector tile encoder.
+ */
+ public VectorTileEncoder() {
+
+ }
+
+ /**
+ * Encodes a tile into a vector tile.
+ *
+ * @param tile The tile to encode
+ * @return The vector tile
+ */
+ public VectorTile.Tile encodeTile(Tile tile) {
+ VectorTile.Tile.Builder builder = VectorTile.Tile.newBuilder();
+ tile.getLayers().forEach(layer -> builder.addLayers(encodeLayer(layer)));
+ return builder.build();
+ }
+
+ /**
+ * Encodes a layer into a vector tile layer.
+ *
+ * @param layer The layer to encode
+ * @return The vector tile layer
+ */
+ public VectorTile.Tile.Layer encodeLayer(Layer layer) {
+ cx = 0;
+ cy = 0;
+
+ keys = new ArrayList<>();
+ values = new ArrayList<>();
+
+ VectorTile.Tile.Layer.Builder builder = VectorTile.Tile.Layer.newBuilder();
+ builder.setName(layer.getName());
+ builder.setVersion(2);
+ builder.setExtent(layer.getExtent());
+
+ // Encode the features
+ layer.getFeatures().stream()
+ .forEach(feature -> encodeFeature(feature, builder::addFeatures));
+
+ // Encode the keys and values
+ builder.addAllKeys(keys);
+ builder.addAllValues(values.stream().map(this::encodeValue).toList());
+
+ return builder.build();
+ }
+
+ /**
+ * Encodes a Java object into a vector tile value.
+ *
+ * @param object The object to encode
+ * @return The vector tile value
+ */
+ protected VectorTile.Tile.Value encodeValue(Object object) {
+ VectorTile.Tile.Value.Builder builder = VectorTile.Tile.Value.newBuilder();
+
+ // Encode the value based on its type
+ if (object instanceof String value) {
+ builder.setStringValue(value);
+ } else if (object instanceof Float value) {
+ builder.setFloatValue(value);
+ } else if (object instanceof Double value) {
+ builder.setDoubleValue(value);
+ } else if (object instanceof Integer value) {
+ builder.setIntValue(value);
+ } else if (object instanceof Long value) {
+ builder.setIntValue(value);
+ } else if (object instanceof Boolean value) {
+ builder.setBoolValue(value);
+ }
+
+ return builder.build();
+ }
+
+ /**
+ * Encode a feature.
+ *
+ * @param feature The feature to encode.
+ */
+ protected void encodeFeature(Feature feature, Consumer<VectorTile.Tile.Feature> consumer) {
+ VectorTile.Tile.Feature.Builder builder = VectorTile.Tile.Feature.newBuilder();
+
+ builder.setId(feature.getId());
+ builder.setType(encodeGeometryType(feature.getGeometry()));
+
+ encodeTag(feature.getTags(), builder::addTags);
+ encodeGeometry(feature.getGeometry(), builder::addGeometry);
+
+ consumer.accept(builder.build());
+ }
+
+ protected VectorTile.Tile.GeomType encodeGeometryType(Geometry geometry) {
+ if (geometry instanceof Point) {
+ return VectorTile.Tile.GeomType.POINT;
+ } else if (geometry instanceof MultiPoint) {
+ return VectorTile.Tile.GeomType.POINT;
+ } else if (geometry instanceof LineString) {
+ return VectorTile.Tile.GeomType.LINESTRING;
+ } else if (geometry instanceof MultiLineString) {
+ return VectorTile.Tile.GeomType.LINESTRING;
+ } else if (geometry instanceof Polygon) {
+ return VectorTile.Tile.GeomType.POLYGON;
+ } else if (geometry instanceof MultiPolygon) {
+ return VectorTile.Tile.GeomType.POLYGON;
+ } else {
+ return VectorTile.Tile.GeomType.UNKNOWN;
+ }
+ }
+
+
+ /**
+ * Encode the tags of a feature.
+ *
+ * @param tags The tags of a feature.
+ * @param encoding The consumer of the tags.
+ */
+ protected void encodeTag(Map<String, Object> tags, Consumer<Integer> encoding) {
+ for (Entry<String, Object> tag : tags.entrySet()) {
+ int keyIndex = keys.indexOf(tag.getKey());
+ if (keyIndex == -1) {
+ keyIndex = keys.size();
+ keys.add(tag.getKey());
+ }
+ int valueIndex = values.indexOf(tag.getValue());
+ if (valueIndex == -1) {
+ valueIndex = values.size();
+ values.add(tag.getValue());
+ }
+ encoding.accept(keyIndex);
+ encoding.accept(valueIndex);
+ }
+ }
+
+ /**
+ * Encode a geometry into a list of commands and parameters.
+ *
+ * @param geometry The geometry to encode.
+ * @param encoding The consumer of commands and parameters.
+ */
+ protected void encodeGeometry(Geometry geometry, Consumer<Integer> encoding) {
+ if (geometry instanceof Point) {
+ encodePoint((Point) geometry, encoding);
+ } else if (geometry instanceof MultiPoint) {
+ encodeMultiPoint((MultiPoint) geometry, encoding);
+ } else if (geometry instanceof LineString) {
+ encodeLineString((LineString) geometry, encoding);
+ } else if (geometry instanceof MultiLineString) {
+ encodeMultiLineString((MultiLineString) geometry, encoding);
+ } else if (geometry instanceof Polygon) {
+ encodePolygon((Polygon) geometry, encoding);
+ } else if (geometry instanceof MultiPolygon) {
+ encodeMultiPolygon((MultiPolygon) geometry, encoding);
+ } else if (geometry instanceof GeometryCollection) {
+ throw new UnsupportedOperationException("GeometryCollection not supported");
+ }
+ }
+
+ /**
+ * Encodes a point into a list of commands and parameters.
+ *
+ * @param point The point to encode.
+ * @param encoding The consumer of commands and parameters.
+ */
+ protected void encodePoint(Point point, Consumer<Integer> encoding) {
+ encoding.accept(command(MOVE_TO, 1));
+ Coordinate coordinate = point.getCoordinate();
+ int dx = (int) Math.round(coordinate.getX()) - cx;
+ int dy = (int) Math.round(coordinate.getY()) - cy;
+ encoding.accept(parameter(dx));
+ encoding.accept(parameter(dy));
+ cx += dx;
+ cy += dy;
+ }
+
+ /**
+ * Encodes a multipoint into a list of commands and parameters.
+ *
+ * @param multiPoint The multipoint to encode.
+ * @param encoding The consumer of commands and parameters.
+ */
+ protected void encodeMultiPoint(MultiPoint multiPoint, Consumer<Integer> encoding) {
+ List<Coordinate> coordinates = List.of(multiPoint.getCoordinates());
+ encoding.accept(command(MOVE_TO, coordinates.size()));
+ encodeCoordinates(coordinates, encoding);
+ }
+
+ /**
+ * Encodes a linestring into a list of commands and parameters.
+ *
+ * @param lineString The linestring to encode.
+ * @param encoding The consumer of commands and parameters.
+ */
+ protected void encodeLineString(LineString lineString, Consumer<Integer> encoding) {
+ List<Coordinate> coordinates = List.of(lineString.getCoordinates());
+ encoding.accept(command(MOVE_TO, 1));
+ encodeCoordinates(coordinates.subList(0, 1), encoding);
+ encoding.accept(command(LINE_TO, coordinates.size() - 1));
+ encodeCoordinates(coordinates.subList(1, coordinates.size()), encoding);
+ }
+
+ /**
+ * Encodes a multilinestring into a list of commands and parameters.
+ *
+ * @param multiLineString The multilinestring to encode.
+ * @param encoding The consumer of commands and parameters.
+ */
+ protected void encodeMultiLineString(MultiLineString multiLineString,
+ Consumer<Integer> encoding) {
+ for (int i = 0; i < multiLineString.getNumGeometries(); i++) {
+ Geometry geometry = multiLineString.getGeometryN(i);
+ if (geometry instanceof LineString lineString) {
+ encodeLineString(lineString, encoding);
+ }
+ }
+ }
+
+ /**
+ * Encodes a polygon into a list of commands and parameters.
+ *
+ * @param polygon The polygon to encode.
+ * @param encoding The consumer of commands and parameters.
+ */
+ protected void encodePolygon(Polygon polygon, Consumer<Integer> encoding) {
+ LinearRing exteriorRing = polygon.getExteriorRing();
+ List<Coordinate> exteriorRingCoordinates = List.of(exteriorRing.getCoordinates());
+
+ // Exterior ring must be clockwise
+ if (isClockWise(exteriorRing)) {
+ exteriorRingCoordinates = Lists.reverse(exteriorRingCoordinates);
+ }
+
+ encodeRing(exteriorRingCoordinates, encoding);
+
+ for (int i = 0; i < polygon.getNumInteriorRing(); i++) {
+ LinearRing interiorRing = polygon.getInteriorRingN(i);
+ List<Coordinate> interiorRingCoordinates = List.of(interiorRing.getCoordinates());
+
+ // Exterior ring must be counter-clockwise
+ if (!isClockWise(interiorRing)) {
+ interiorRingCoordinates = Lists.reverse(exteriorRingCoordinates);
+ }
+
+ encodeRing(interiorRingCoordinates, encoding);
+ }
+ }
+
+ /**
+ * Encodes a ring into a list of commands and parameters.
+ *
+ * @param coordinates The coordinates of the ring
+ * @param encoding The consumer of commands and parameters
+ */
+ protected void encodeRing(List<Coordinate> coordinates, Consumer<Integer> encoding) {
+ // Move to first point
+ List<Coordinate> head = coordinates.subList(0, 1);
+ encoding.accept(command(MOVE_TO, 1));
+ encodeCoordinates(head, encoding);
+
+ // Line to remaining points
+ List<Coordinate> tail = coordinates.subList(1, coordinates.size() - 1);
+ encoding.accept(command(LINE_TO, tail.size()));
+ encodeCoordinates(tail, encoding);
+
+ // Close the ring
+ encoding.accept(command(CLOSE_PATH, 1));
+ }
+
+ /**
+ * Encodes a multipolygon into a list of commands and parameters.
+ *
+ * @param multiPolygon The multipolygon to encode
+ * @param encoding The consumer of commands and parameters
+ */
+ protected void encodeMultiPolygon(MultiPolygon multiPolygon, Consumer<Integer> encoding) {
+ for (int i = 0; i < multiPolygon.getNumGeometries(); i++) {
+ Geometry geometry = multiPolygon.getGeometryN(i);
+ if (geometry instanceof Polygon polygon) {
+ encodePolygon(polygon, encoding);
+ }
+ }
+ }
+
+ /**
+ * Encodes a list of coordinates into a list of parameters.
+ *
+ * @param coordinates The coordinates to encode
+ * @param encoding The consumer of parameters
+ */
+ protected void encodeCoordinates(List<Coordinate> coordinates, Consumer<Integer> encoding) {
+ for (Coordinate coordinate : coordinates) {
+ int dx = (int) Math.round(coordinate.getX()) - cx;
+ int dy = (int) Math.round(coordinate.getY()) - cy;
+ encoding.accept(parameter(dx));
+ encoding.accept(parameter(dy));
+ cx += dx;
+ cy += dy;
+ }
+ }
+
+ /**
+ * Encodes a command.
+ *
+ * @param id The command id
+ * @param count The number of parameters
+ * @return The encoded command
+ */
+ protected static int command(int id, int count) {
+ return (id & 0x7) | (count << 3);
+ }
+
+ /**
+ * Encodes a parameter.
+ *
+ * @param value The parameter value
+ * @return The encoded parameter
+ */
+ protected static int parameter(int value) {
+ return (value << 1) ^ (value >> 31);
+ }
+}
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/vectortile/VectorTileUtils.java b/baremaps-core/src/main/java/org/apache/baremaps/vectortile/VectorTileUtils.java
new file mode 100644
index 00000000..e44cefa1
--- /dev/null
+++ b/baremaps-core/src/main/java/org/apache/baremaps/vectortile/VectorTileUtils.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.baremaps.vectortile;
+
+import java.nio.ByteBuffer;
+import org.locationtech.jts.algorithm.Orientation;
+import org.locationtech.jts.geom.Envelope;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.util.AffineTransformation;
+
+/**
+ * Utility class for vector tiles.
+ */
+public class VectorTileUtils {
+
+ public static final int MOVE_TO = 1;
+
+ public static final int LINE_TO = 2;
+
+ public static final int CLOSE_PATH = 7;
+
+ /**
+ * Transforms a geometry into a vector tile geometry.
+ *
+ * @param geometry The geometry to transform
+ * @param envelope The envelope of the tile
+ * @param extent The extent of the tile
+ * @param buffer The buffer of the tile
+ * @param clipGeom A flag to clip the geometry
+ * @return The transformed geometry
+ */
+ public static Geometry asVectorTileGeom(Geometry geometry, Envelope envelope, int extent,
+ int buffer, boolean clipGeom) {
+
+ // Scale the geometry to the extent of the tile
+ double scaleX = extent / envelope.getWidth();
+ double scaleY = extent / envelope.getHeight();
+ AffineTransformation affineTransformation = new AffineTransformation();
+ affineTransformation.translate(-envelope.getMinX(), -envelope.getMinY());
+ affineTransformation.scale(scaleX, -scaleY);
+ affineTransformation.translate(0, extent);
+ Geometry scaledGeometry = affineTransformation.transform(geometry);
+
+ // Build the final geometry
+ if (clipGeom) {
+ return clipToTile(scaledGeometry, extent, buffer);
+ } else {
+ return scaledGeometry;
+ }
+ }
+
+ /**
+ * Transforms a tile into a vector tile.
+ *
+ * @param tile The tile to transform
+ * @return The transformed tile
+ */
+ public static ByteBuffer asVectorTile(Tile tile) {
+ return new VectorTileEncoder()
+ .encodeTile(tile)
+ .toByteString()
+ .asReadOnlyByteBuffer();
+ }
+
+ /**
+ * Transforms a layer into a vector tile layer.
+ *
+ * @param layer The layer to transform
+ * @return The transformed layer
+ */
+ public static ByteBuffer asVectorTileLayer(Layer layer) {
+ return new VectorTileEncoder()
+ .encodeLayer(layer)
+ .toByteString()
+ .asReadOnlyByteBuffer();
+ }
+
+ /**
+ * Clips a geometry to a tile.
+ *
+ * @param geometry The geometry to clip
+ * @param extent The extent of the tile
+ * @param buffer The buffer of the tile
+ * @return The clipped geometry
+ */
+ private static Geometry clipToTile(Geometry geometry, int extent, int buffer) {
+ Envelope envelope = new Envelope(0 - buffer, extent + buffer, 0 - buffer, extent + buffer);
+ GeometryFactory geometryFactory = new GeometryFactory();
+ Geometry tile = geometryFactory.toGeometry(envelope);
+ return geometry.intersection(tile);
+ }
+
+
+ /**
+ * Returns true if the winding order of the vector tile geometry is clockwise.
+ *
+ * @param geometry The vector tile geometry
+ * @return True if the winding order is clockwise
+ */
+ public static boolean isClockWise(Geometry geometry) {
+ // As the origin of the vector tile coordinate system is in the top left corner, the
+ // orientation of the geometry is inverted.
+ return Orientation.isCCW(geometry.getCoordinates());
+ }
+}
diff --git a/baremaps-core/src/main/proto/vector_tile.proto b/baremaps-core/src/main/proto/vector_tile.proto
new file mode 100644
index 00000000..094e5d43
--- /dev/null
+++ b/baremaps-core/src/main/proto/vector_tile.proto
@@ -0,0 +1,402 @@
+/*
+Creative Commons Legal Code
+
+Attribution 3.0 Unported
+
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+ LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+ REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR
+ DAMAGES RESULTING FROM ITS USE.
+
+License
+
+THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE
+COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY
+COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS
+AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
+
+BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE
+TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY
+BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS
+CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND
+CONDITIONS.
+
+1. Definitions
+
+ a. "Adaptation" means a work based upon the Work, or upon the Work and
+ other pre-existing works, such as a translation, adaptation,
+ derivative work, arrangement of music or other alterations of a
+ literary or artistic work, or phonogram or performance and includes
+ cinematographic adaptations or any other form in which the Work may be
+ recast, transformed, or adapted including in any form recognizably
+ derived from the original, except that a work that constitutes a
+ Collection will not be considered an Adaptation for the purpose of
+ this License. For the avoidance of doubt, where the Work is a musical
+ work, performance or phonogram, the synchronization of the Work in
+ timed-relation with a moving image ("synching") will be considered an
+ Adaptation for the purpose of this License.
+ b. "Collection" means a collection of literary or artistic works, such as
+ encyclopedias and anthologies, or performances, phonograms or
+ broadcasts, or other works or subject matter other than works listed
+ in Section 1(f) below, which, by reason of the selection and
+ arrangement of their contents, constitute intellectual creations, in
+ which the Work is included in its entirety in unmodified form along
+ with one or more other contributions, each constituting separate and
+ independent works in themselves, which together are assembled into a
+ collective whole. A work that constitutes a Collection will not be
+ considered an Adaptation (as defined above) for the purposes of this
+ License.
+ c. "Distribute" means to make available to the public the original and
+ copies of the Work or Adaptation, as appropriate, through sale or
+ other transfer of ownership.
+ d. "Licensor" means the individual, individuals, entity or entities that
+ offer(s) the Work under the terms of this License.
+ e. "Original Author" means, in the case of a literary or artistic work,
+ the individual, individuals, entity or entities who created the Work
+ or if no individual or entity can be identified, the publisher; and in
+ addition (i) in the case of a performance the actors, singers,
+ musicians, dancers, and other persons who act, sing, deliver, declaim,
+ play in, interpret or otherwise perform literary or artistic works or
+ expressions of folklore; (ii) in the case of a phonogram the producer
+ being the person or legal entity who first fixes the sounds of a
+ performance or other sounds; and, (iii) in the case of broadcasts, the
+ organization that transmits the broadcast.
+ f. "Work" means the literary and/or artistic work offered under the terms
+ of this License including without limitation any production in the
+ literary, scientific and artistic domain, whatever may be the mode or
+ form of its expression including digital form, such as a book,
+ pamphlet and other writing; a lecture, address, sermon or other work
+ of the same nature; a dramatic or dramatico-musical work; a
+ choreographic work or entertainment in dumb show; a musical
+ composition with or without words; a cinematographic work to which are
+ assimilated works expressed by a process analogous to cinematography;
+ a work of drawing, painting, architecture, sculpture, engraving or
+ lithography; a photographic work to which are assimilated works
+ expressed by a process analogous to photography; a work of applied
+ art; an illustration, map, plan, sketch or three-dimensional work
+ relative to geography, topography, architecture or science; a
+ performance; a broadcast; a phonogram; a compilation of data to the
+ extent it is protected as a copyrightable work; or a work performed by
+ a variety or circus performer to the extent it is not otherwise
+ considered a literary or artistic work.
+ g. "You" means an individual or entity exercising rights under this
+ License who has not previously violated the terms of this License with
+ respect to the Work, or who has received express permission from the
+ Licensor to exercise rights under this License despite a previous
+ violation.
+ h. "Publicly Perform" means to perform public recitations of the Work and
+ to communicate to the public those public recitations, by any means or
+ process, including by wire or wireless means or public digital
+ performances; to make available to the public Works in such a way that
+ members of the public may access these Works from a place and at a
+ place individually chosen by them; to perform the Work to the public
+ by any means or process and the communication to the public of the
+ performances of the Work, including by public digital performance; to
+ broadcast and rebroadcast the Work by any means including signs,
+ sounds or images.
+ i. "Reproduce" means to make copies of the Work by any means including
+ without limitation by sound or visual recordings and the right of
+ fixation and reproducing fixations of the Work, including storage of a
+ protected performance or phonogram in digital form or other electronic
+ medium.
+
+2. Fair Dealing Rights. Nothing in this License is intended to reduce,
+limit, or restrict any uses free from copyright or rights arising from
+limitations or exceptions that are provided for in connection with the
+copyright protection under copyright law or other applicable laws.
+
+3. License Grant. Subject to the terms and conditions of this License,
+Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
+perpetual (for the duration of the applicable copyright) license to
+exercise the rights in the Work as stated below:
+
+ a. to Reproduce the Work, to incorporate the Work into one or more
+ Collections, and to Reproduce the Work as incorporated in the
+ Collections;
+ b. to create and Reproduce Adaptations provided that any such Adaptation,
+ including any translation in any medium, takes reasonable steps to
+ clearly label, demarcate or otherwise identify that changes were made
+ to the original Work. For example, a translation could be marked "The
+ original work was translated from English to Spanish," or a
+ modification could indicate "The original work has been modified.";
+ c. to Distribute and Publicly Perform the Work including as incorporated
+ in Collections; and,
+ d. to Distribute and Publicly Perform Adaptations.
+ e. For the avoidance of doubt:
+
+ i. Non-waivable Compulsory License Schemes. In those jurisdictions in
+ which the right to collect royalties through any statutory or
+ compulsory licensing scheme cannot be waived, the Licensor
+ reserves the exclusive right to collect such royalties for any
+ exercise by You of the rights granted under this License;
+ ii. Waivable Compulsory License Schemes. In those jurisdictions in
+ which the right to collect royalties through any statutory or
+ compulsory licensing scheme can be waived, the Licensor waives the
+ exclusive right to collect such royalties for any exercise by You
+ of the rights granted under this License; and,
+ iii. Voluntary License Schemes. The Licensor waives the right to
+ collect royalties, whether individually or, in the event that the
+ Licensor is a member of a collecting society that administers
+ voluntary licensing schemes, via that society, from any exercise
+ by You of the rights granted under this License.
+
+The above rights may be exercised in all media and formats whether now
+known or hereafter devised. The above rights include the right to make
+such modifications as are technically necessary to exercise the rights in
+other media and formats. Subject to Section 8(f), all rights not expressly
+granted by Licensor are hereby reserved.
+
+4. Restrictions. The license granted in Section 3 above is expressly made
+subject to and limited by the following restrictions:
+
+ a. You may Distribute or Publicly Perform the Work only under the terms
+ of this License. You must include a copy of, or the Uniform Resource
+ Identifier (URI) for, this License with every copy of the Work You
+ Distribute or Publicly Perform. You may not offer or impose any terms
+ on the Work that restrict the terms of this License or the ability of
+ the recipient of the Work to exercise the rights granted to that
+ recipient under the terms of the License. You may not sublicense the
+ Work. You must keep intact all notices that refer to this License and
+ to the disclaimer of warranties with every copy of the Work You
+ Distribute or Publicly Perform. When You Distribute or Publicly
+ Perform the Work, You may not impose any effective technological
+ measures on the Work that restrict the ability of a recipient of the
+ Work from You to exercise the rights granted to that recipient under
+ the terms of the License. This Section 4(a) applies to the Work as
+ incorporated in a Collection, but this does not require the Collection
+ apart from the Work itself to be made subject to the terms of this
+ License. If You create a Collection, upon notice from any Licensor You
+ must, to the extent practicable, remove from the Collection any credit
+ as required by Section 4(b), as requested. If You create an
+ Adaptation, upon notice from any Licensor You must, to the extent
+ practicable, remove from the Adaptation any credit as required by
+ Section 4(b), as requested.
+ b. If You Distribute, or Publicly Perform the Work or any Adaptations or
+ Collections, You must, unless a request has been made pursuant to
+ Section 4(a), keep intact all copyright notices for the Work and
+ provide, reasonable to the medium or means You are utilizing: (i) the
+ name of the Original Author (or pseudonym, if applicable) if supplied,
+ and/or if the Original Author and/or Licensor designate another party
+ or parties (e.g., a sponsor institute, publishing entity, journal) for
+ attribution ("Attribution Parties") in Licensor's copyright notice,
+ terms of service or by other reasonable means, the name of such party
+ or parties; (ii) the title of the Work if supplied; (iii) to the
+ extent reasonably practicable, the URI, if any, that Licensor
+ specifies to be associated with the Work, unless such URI does not
+ refer to the copyright notice or licensing information for the Work;
+ and (iv) , consistent with Section 3(b), in the case of an Adaptation,
+ a credit identifying the use of the Work in the Adaptation (e.g.,
+ "French translation of the Work by Original Author," or "Screenplay
+ based on original Work by Original Author"). The credit required by
+ this Section 4 (b) may be implemented in any reasonable manner;
+ provided, however, that in the case of a Adaptation or Collection, at
+ a minimum such credit will appear, if a credit for all contributing
+ authors of the Adaptation or Collection appears, then as part of these
+ credits and in a manner at least as prominent as the credits for the
+ other contributing authors. For the avoidance of doubt, You may only
+ use the credit required by this Section for the purpose of attribution
+ in the manner set out above and, by exercising Your rights under this
+ License, You may not implicitly or explicitly assert or imply any
+ connection with, sponsorship or endorsement by the Original Author,
+ Licensor and/or Attribution Parties, as appropriate, of You or Your
+ use of the Work, without the separate, express prior written
+ permission of the Original Author, Licensor and/or Attribution
+ Parties.
+ c. Except as otherwise agreed in writing by the Licensor or as may be
+ otherwise permitted by applicable law, if You Reproduce, Distribute or
+ Publicly Perform the Work either by itself or as part of any
+ Adaptations or Collections, You must not distort, mutilate, modify or
+ take other derogatory action in relation to the Work which would be
+ prejudicial to the Original Author's honor or reputation. Licensor
+ agrees that in those jurisdictions (e.g. Japan), in which any exercise
+ of the right granted in Section 3(b) of this License (the right to
+ make Adaptations) would be deemed to be a distortion, mutilation,
+ modification or other derogatory action prejudicial to the Original
+ Author's honor and reputation, the Licensor will waive or not assert,
+ as appropriate, this Section, to the fullest extent permitted by the
+ applicable national law, to enable You to reasonably exercise Your
+ right under Section 3(b) of this License (right to make Adaptations)
+ but not otherwise.
+
+5. Representations, Warranties and Disclaimer
+
+UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR
+OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY
+KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE,
+INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY,
+FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF
+LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS,
+WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION
+OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
+
+6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE
+LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR
+ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES
+ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS
+BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+7. Termination
+
+ a. This License and the rights granted hereunder will terminate
+ automatically upon any breach by You of the terms of this License.
+ Individuals or entities who have received Adaptations or Collections
+ from You under this License, however, will not have their licenses
+ terminated provided such individuals or entities remain in full
+ compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will
+ survive any termination of this License.
+ b. Subject to the above terms and conditions, the license granted here is
+ perpetual (for the duration of the applicable copyright in the Work).
+ Notwithstanding the above, Licensor reserves the right to release the
+ Work under different license terms or to stop distributing the Work at
+ any time; provided, however that any such election will not serve to
+ withdraw this License (or any other license that has been, or is
+ required to be, granted under the terms of this License), and this
+ License will continue in full force and effect unless terminated as
+ stated above.
+
+8. Miscellaneous
+
+ a. Each time You Distribute or Publicly Perform the Work or a Collection,
+ the Licensor offers to the recipient a license to the Work on the same
+ terms and conditions as the license granted to You under this License.
+ b. Each time You Distribute or Publicly Perform an Adaptation, Licensor
+ offers to the recipient a license to the original Work on the same
+ terms and conditions as the license granted to You under this License.
+ c. If any provision of this License is invalid or unenforceable under
+ applicable law, it shall not affect the validity or enforceability of
+ the remainder of the terms of this License, and without further action
+ by the parties to this agreement, such provision shall be reformed to
+ the minimum extent necessary to make such provision valid and
+ enforceable.
+ d. No term or provision of this License shall be deemed waived and no
+ breach consented to unless such waiver or consent shall be in writing
+ and signed by the party to be charged with such waiver or consent.
+ e. This License constitutes the entire agreement between the parties with
+ respect to the Work licensed here. There are no understandings,
+ agreements or representations with respect to the Work not specified
+ here. Licensor shall not be bound by any additional provisions that
+ may appear in any communication from You. This License may not be
+ modified without the mutual written agreement of the Licensor and You.
+ f. The rights granted under, and the subject matter referenced, in this
+ License were drafted utilizing the terminology of the Berne Convention
+ for the Protection of Literary and Artistic Works (as amended on
+ September 28, 1979), the Rome Convention of 1961, the WIPO Copyright
+ Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996
+ and the Universal Copyright Convention (as revised on July 24, 1971).
+ These rights and subject matter take effect in the relevant
+ jurisdiction in which the License terms are sought to be enforced
+ according to the corresponding provisions of the implementation of
+ those treaty provisions in the applicable national law. If the
+ standard suite of rights granted under applicable copyright law
+ includes additional rights not granted under this License, such
+ additional rights are deemed to be included in the License; this
+ License is not intended to restrict the license of any rights under
+ applicable law.
+
+
+Creative Commons Notice
+
+ Creative Commons is not a party to this License, and makes no warranty
+ whatsoever in connection with the Work. Creative Commons will not be
+ liable to You or any party on any legal theory for any damages
+ whatsoever, including without limitation any general, special,
+ incidental or consequential damages arising in connection to this
+ license. Notwithstanding the foregoing two (2) sentences, if Creative
+ Commons has expressly identified itself as the Licensor hereunder, it
+ shall have all rights and obligations of Licensor.
+
+ Except for the limited purpose of indicating to the public that the
+ Work is licensed under the CCPL, Creative Commons does not authorize
+ the use by either party of the trademark "Creative Commons" or any
+ related trademark or logo of Creative Commons without the prior
+ written consent of Creative Commons. Any permitted use will be in
+ compliance with Creative Commons' then-current trademark usage
+ guidelines, as may be published on its website or otherwise made
+ available upon request from time to time. For the avoidance of doubt,
+ this trademark restriction does not form part of this License.
+
+ Creative Commons may be contacted at https://creativecommons.org/.
+*/
+syntax = "proto2";
+
+option optimize_for = SPEED;
+option java_package = "org.apache.baremaps.mvt.binary";
+
+package vector_tile;
+
+message Tile {
+
+ // GeomType is described in section 4.3.4 of the specification
+ enum GeomType {
+ UNKNOWN = 0;
+ POINT = 1;
+ LINESTRING = 2;
+ POLYGON = 3;
+ }
+
+ // Variant type encoding
+ // The use of values is described in section 4.1 of the specification
+ message Value {
+ // Exactly one of these values must be present in a valid message
+ optional string string_value = 1;
+ optional float float_value = 2;
+ optional double double_value = 3;
+ optional int64 int_value = 4;
+ optional uint64 uint_value = 5;
+ optional sint64 sint_value = 6;
+ optional bool bool_value = 7;
+
+ extensions 8 to max;
+ }
+
+ // Features are described in section 4.2 of the specification
+ message Feature {
+ optional uint64 id = 1 [ default = 0 ];
+
+ // Tags of this feature are encoded as repeated pairs of
+ // integers.
+ // A detailed description of tags is located in sections
+ // 4.2 and 4.4 of the specification
+ repeated uint32 tags = 2 [ packed = true ];
+
+ // The type of geometry stored in this feature.
+ optional GeomType type = 3 [ default = UNKNOWN ];
+
+ // Contains a stream of commands and parameters (vertices).
+ // A detailed description on geometry encoding is located in
+ // section 4.3 of the specification.
+ repeated uint32 geometry = 4 [ packed = true ];
+ }
+
+ // Layers are described in section 4.1 of the specification
+ message Layer {
+ // Any compliant implementation must first read the version
+ // number encoded in this message and choose the correct
+ // implementation for this version number before proceeding to
+ // decode other parts of this message.
+ required uint32 version = 15 [ default = 1 ];
+
+ required string name = 1;
+
+ // The actual features in this tile.
+ repeated Feature features = 2;
+
+ // Dictionary encoding for keys
+ repeated string keys = 3;
+
+ // Dictionary encoding for values
+ repeated Value values = 4;
+
+ // Although this is an "optional" field it is required by the specification.
+ // See https://github.com/mapbox/vector-tile-spec/issues/47
+ optional uint32 extent = 5 [ default = 4096 ];
+
+ extensions 16 to max;
+ }
+
+ repeated Layer layers = 3;
+
+ extensions 16 to 8191;
+}
\ No newline at end of file
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/vectortile/VectorTileDecoderTest.java b/baremaps-core/src/test/java/org/apache/baremaps/vectortile/VectorTileDecoderTest.java
new file mode 100644
index 00000000..7c171cd5
--- /dev/null
+++ b/baremaps-core/src/test/java/org/apache/baremaps/vectortile/VectorTileDecoderTest.java
@@ -0,0 +1,170 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.baremaps.vectortile;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.locationtech.jts.geom.*;
+
+class VectorTileDecoderTest {
+
+ private final GeometryFactory geometryFactory = new GeometryFactory();
+
+ /**
+ * An example encoding of a point located at:
+ *
+ * (25,17)
+ */
+ @Test
+ public void decodePoint() {
+ var coordinate = new Coordinate(25, 17);
+ var point = geometryFactory.createPoint(coordinate);
+ var decoder = new VectorTileDecoder();
+ var encoding = List.of(9, 50, 34);
+ assertEquals(point, decoder.decodePoint(encoding));
+ }
+
+ /**
+ * 4.3.5.2. Example Multi Point
+ *
+ * An example encoding of two points located at:
+ *
+ * (5,7) (3,2)
+ */
+ @Test
+ public void decodeMultiPoint() {
+ var coordinates = new Coordinate[] {
+ new Coordinate(5, 7),
+ new Coordinate(3, 2)
+ };
+ var multiPoint = geometryFactory.createMultiPoint(coordinates);
+ var decoder = new VectorTileDecoder();
+ var encoding = List.of(17, 10, 14, 3, 9);
+ assertEquals(multiPoint, decoder.decodePoint(encoding));
+ }
+
+ /**
+ * 4.3.5.3. Example Linestring
+ *
+ * An example encoding of a line with the points:
+ *
+ * (2,2) (2,10) (10,10)
+ */
+ @Test
+ public void decodeLineString() {
+ var lineString = geometryFactory.createLineString(new Coordinate[] {
+ new Coordinate(2, 2),
+ new Coordinate(2, 10),
+ new Coordinate(10, 10)
+ });
+ var decoder = new VectorTileDecoder();
+ assertEquals(lineString, decoder.decodeLineString(List.of(9, 4, 4, 18, 0, 16, 16, 0)));
+ }
+
+ /**
+ * 4.3.5.4. Example Multi Linestring
+ * <p>
+ * An example encoding of two lines with the points:
+ * <p>
+ * Line 1: - (2,2) - (2,10) - (10,10) Line 2: - (1,1) - (3,5)
+ */
+ @Test
+ public void decodeMultiLineString() {
+ var lineString1 = geometryFactory.createLineString(new Coordinate[] {
+ new Coordinate(2, 2),
+ new Coordinate(2, 10),
+ new Coordinate(10, 10)
+ });;
+ var lineString2 = geometryFactory.createLineString(new Coordinate[] {
+ new Coordinate(1, 1),
+ new Coordinate(3, 5)
+ });
+ var multiLineString =
+ geometryFactory.createMultiLineString(new LineString[] {lineString1, lineString2});
+ var decoder = new VectorTileDecoder();
+ var encoding = List.of(9, 4, 4, 18, 0, 16, 16, 0, 9, 17, 17, 10, 4, 8);
+ assertEquals(multiLineString, decoder.decodeLineString(encoding));
+ }
+
+ /**
+ * 4.3.5.5. Example Polygon
+ * <p>
+ * An example encoding of a polygon feature that has the points:
+ * <p>
+ * (3,6) (8,12) (20,34) (3,6) Path Closing as Last Point
+ */
+ @Test
+ public void decodePolygon() {
+ var polygon = geometryFactory.createPolygon(new Coordinate[] {
+ new Coordinate(3, 6),
+ new Coordinate(8, 12),
+ new Coordinate(20, 34),
+ new Coordinate(3, 6)
+ });
+ var decoder = new VectorTileDecoder();
+ var encoding = List.of(9, 6, 12, 18, 10, 12, 24, 44, 15);
+ assertEquals(polygon, decoder.decodePolygon(encoding));
+ }
+
+ /**
+ * 4.3.5.6. Example Multi Polygon An example of a more complex encoding of two polygons, one with
+ * a hole. The position of the points for the polygons are shown below. The winding order of the
+ * polygons is VERY important in this example as it signifies the difference between interior
+ * rings and a new polygon.
+ * <p>
+ * Polygon 1: Exterior Ring: (0,0) (10,0) (10,10) (0,10) (0,0) Path Closing as Last Point Polygon
+ * 2: Exterior Ring: (11,11) (20,11) (20,20) (11,20) (11,11) Path Closing as Last Point Interior
+ * Ring: (13,13) (13,17) (17,17) (17,13) (13,13) Path Closing as Last Point
+ */
+ @Test
+ public void decodeMultiPolygon() {
+ var multiPolygon = geometryFactory.createMultiPolygon(
+ new Polygon[] {
+ geometryFactory.createPolygon(
+ new Coordinate[] {
+ new Coordinate(0, 0),
+ new Coordinate(10, 0),
+ new Coordinate(10, 10),
+ new Coordinate(0, 10),
+ new Coordinate(0, 0)
+ }),
+ geometryFactory.createPolygon(
+ geometryFactory.createLinearRing(
+ new Coordinate[] {
+ new Coordinate(11, 11),
+ new Coordinate(20, 11),
+ new Coordinate(20, 20),
+ new Coordinate(11, 20),
+ new Coordinate(11, 11)
+ }),
+ new LinearRing[] {
+ geometryFactory.createLinearRing(
+ new Coordinate[] {
+ new Coordinate(13, 13),
+ new Coordinate(13, 17),
+ new Coordinate(17, 17),
+ new Coordinate(17, 13),
+ new Coordinate(13, 13)
+ })
+ })
+ });
+ var decoder = new VectorTileDecoder();
+ var encoding = List.of(9, 0, 0, 26, 20, 0, 0, 20, 19, 0, 15, 9, 22, 2, 26, 18, 0, 0, 18, 17, 0,
+ 15, 9, 4, 13, 26, 0, 8, 8, 0, 0, 7, 15);
+ assertEquals(multiPolygon, decoder.decodePolygon(encoding));
+
+
+ }
+}
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/vectortile/VectorTileEncoderTest.java b/baremaps-core/src/test/java/org/apache/baremaps/vectortile/VectorTileEncoderTest.java
new file mode 100644
index 00000000..78d4d326
--- /dev/null
+++ b/baremaps-core/src/test/java/org/apache/baremaps/vectortile/VectorTileEncoderTest.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.baremaps.vectortile;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.locationtech.jts.geom.*;
+
+class VectorTileEncoderTest {
+
+ private final GeometryFactory geometryFactory = new GeometryFactory();
+
+ /**
+ * An example encoding of a point located at:
+ *
+ * (25,17)
+ */
+ @Test
+ public void encodePoint() {
+ var coordinate = new Coordinate(25, 17);
+ var point = geometryFactory.createPoint(coordinate);
+ var encoder = new VectorTileEncoder();
+ var encoding = new ArrayList<Integer>();
+ encoder.encodePoint(point, encoding::add);
+ assertEquals(List.of(9, 50, 34), encoding);
+ }
+
+ /**
+ * 4.3.5.2. Example Multi Point
+ *
+ * An example encoding of two points located at:
+ *
+ * (5,7) (3,2)
+ */
+ @Test
+ public void encodeMultiPoint() {
+ var coordinates = new Coordinate[] {
+ new Coordinate(5, 7),
+ new Coordinate(3, 2)
+ };
+ var multiPoint = geometryFactory.createMultiPoint(coordinates);
+ var encoder = new VectorTileEncoder();
+ var encoding = new ArrayList<Integer>();
+ encoder.encodeMultiPoint(multiPoint, encoding::add);
+ assertEquals(List.of(17, 10, 14, 3, 9), encoding);
+ }
+
+ /**
+ * 4.3.5.3. Example Linestring
+ *
+ * An example encoding of a line with the points:
+ *
+ * (2,2) (2,10) (10,10)
+ */
+ @Test
+ public void encodeLineString() {
+ var lineString = geometryFactory.createLineString(new Coordinate[] {
+ new Coordinate(2, 2),
+ new Coordinate(2, 10),
+ new Coordinate(10, 10)
+ });
+ var encoder = new VectorTileEncoder();
+ var encoding = new ArrayList<Integer>();
+ encoder.encodeLineString(lineString, encoding::add);
+ assertEquals(List.of(9, 4, 4, 18, 0, 16, 16, 0), encoding);
+ }
+
+ /**
+ * 4.3.5.4. Example Multi Linestring
+ * <p>
+ * An example encoding of two lines with the points:
+ * <p>
+ * Line 1: - (2,2) - (2,10) - (10,10) Line 2: - (1,1) - (3,5)
+ */
+ @Test
+ public void encodeMultiLineString() {
+ var lineString1 = geometryFactory.createLineString(new Coordinate[] {
+ new Coordinate(2, 2),
+ new Coordinate(2, 10),
+ new Coordinate(10, 10)
+ });;
+ var lineString2 = geometryFactory.createLineString(new Coordinate[] {
+ new Coordinate(1, 1),
+ new Coordinate(3, 5)
+ });
+ var multiLineString =
+ geometryFactory.createMultiLineString(new LineString[] {lineString1, lineString2});
+ var encoder = new VectorTileEncoder();
+ var encoding = new ArrayList<Integer>();
+ encoder.encodeMultiLineString(multiLineString, encoding::add);
+ assertEquals(List.of(9, 4, 4, 18, 0, 16, 16, 0, 9, 17, 17, 10, 4, 8), encoding);
+ }
+
+ /**
+ * 4.3.5.5. Example Polygon
+ * <p>
+ * An example encoding of a polygon feature that has the points:
+ * <p>
+ * (3,6) (8,12) (20,34) (3,6) Path Closing as Last Point
+ */
+ @Test
+ public void encodePolygon() {
+ var polygon = geometryFactory.createPolygon(new Coordinate[] {
+ new Coordinate(3, 6),
+ new Coordinate(8, 12),
+ new Coordinate(20, 34),
+ new Coordinate(3, 6)
+ });
+ var encoder = new VectorTileEncoder();
+ var encoding = new ArrayList<Integer>();
+ encoder.encodePolygon(polygon, encoding::add);
+ assertEquals(List.of(9, 6, 12, 18, 10, 12, 24, 44, 15), encoding);
+ }
+
+ /**
+ * 4.3.5.6. Example Multi Polygon An example of a more complex encoding of two polygons, one with
+ * a hole. The position of the points for the polygons are shown below. The winding order of the
+ * polygons is VERY important in this example as it signifies the difference between interior
+ * rings and a new polygon.
+ * <p>
+ * Polygon 1: Exterior Ring: (0,0) (10,0) (10,10) (0,10) (0,0) Path Closing as Last Point Polygon
+ * 2: Exterior Ring: (11,11) (20,11) (20,20) (11,20) (11,11) Path Closing as Last Point Interior
+ * Ring: (13,13) (13,17) (17,17) (17,13) (13,13) Path Closing as Last Point
+ */
+ @Test
+ public void encodeMultiPolygon() {
+ var multiPolygon = geometryFactory.createMultiPolygon(
+ new Polygon[] {
+ geometryFactory.createPolygon(
+ new Coordinate[] {
+ new Coordinate(0, 0),
+ new Coordinate(10, 0),
+ new Coordinate(10, 10),
+ new Coordinate(0, 10),
+ new Coordinate(0, 0)
+ }),
+ geometryFactory.createPolygon(
+ geometryFactory.createLinearRing(
+ new Coordinate[] {
+ new Coordinate(11, 11),
+ new Coordinate(20, 11),
+ new Coordinate(20, 20),
+ new Coordinate(11, 20),
+ new Coordinate(11, 11)
+ }),
+ new LinearRing[] {
+ geometryFactory.createLinearRing(
+ new Coordinate[] {
+ new Coordinate(13, 13),
+ new Coordinate(13, 17),
+ new Coordinate(17, 17),
+ new Coordinate(17, 13),
+ new Coordinate(13, 13)
+ })
+ })
+ });
+ var encoder = new VectorTileEncoder();
+ var encoding = new ArrayList<Integer>();
+ encoder.encodeMultiPolygon(multiPolygon, encoding::add);
+ assertEquals(List.of(9, 0, 0, 26, 20, 0, 0, 20, 19, 0, 15, 9, 22, 2, 26, 18, 0, 0, 18, 17, 0,
+ 15, 9, 4, 13, 26, 0, 8, 8, 0, 0, 7, 15), encoding);
+
+
+ }
+}
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/vectortile/VectorTileTest.java b/baremaps-core/src/test/java/org/apache/baremaps/vectortile/VectorTileTest.java
new file mode 100644
index 00000000..a526d93f
--- /dev/null
+++ b/baremaps-core/src/test/java/org/apache/baremaps/vectortile/VectorTileTest.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.baremaps.vectortile;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.GeometryFactory;
+
+public class VectorTileTest {
+
+ private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();
+
+ @Test
+ public void endToEnd() {
+ var tile = new Tile(List.of(
+ new Layer("layer", 256, List.of(
+ new Feature(1, Map.of("a", 1.0, "b", "2"),
+ GEOMETRY_FACTORY.createPoint(new Coordinate(1, 2))),
+ new Feature(2, Map.of("c", 3.0, "d", "4"),
+ GEOMETRY_FACTORY.createPoint(new Coordinate(2, 3)))))));
+
+ var encoded = new VectorTileEncoder().encodeTile(tile);
+ var decoded = new VectorTileDecoder().decodeTile(encoded);
+
+ assertEquals(tile, decoded);
+ }
+
+}
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/vectortile/VectorTileUtilsTest.java b/baremaps-core/src/test/java/org/apache/baremaps/vectortile/VectorTileUtilsTest.java
new file mode 100644
index 00000000..28df71d4
--- /dev/null
+++ b/baremaps-core/src/test/java/org/apache/baremaps/vectortile/VectorTileUtilsTest.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.baremaps.vectortile;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+import org.locationtech.jts.geom.*;
+
+class VectorTileUtilsTest {
+
+ @Test
+ void asMvtGeom() {
+ // Create a test geometry (a simple square)
+ var coordinates = new Coordinate[] {
+ new Coordinate(1, 1),
+ new Coordinate(5, 9),
+ new Coordinate(9, 1),
+ new Coordinate(1, 1)
+ };
+ var geometryFactory = new GeometryFactory();
+ var inputGeom = geometryFactory.createPolygon(coordinates);
+
+ // Define the tile envelope, extent, buffer, and clipping flag
+ var envelope = new Envelope(0, 10, 0, 10);
+ var extent = 100;
+ var buffer = 10;
+ var clipGeom = true;
+
+ // Transform the input geometry using asMvtGeom
+ var outputGeom =
+ VectorTileUtils.asVectorTileGeom(inputGeom, envelope, extent, buffer, clipGeom);
+
+ // Check if the output geometry is not null
+ assertNotNull(outputGeom);
+
+ // Check if the output geometry is a valid Geometry
+ assertTrue(outputGeom.isValid());
+
+ // Define expected coordinates for the transformed geometry
+ Coordinate[] expectedCoordinates = new Coordinate[] {
+ new Coordinate(10, 90),
+ new Coordinate(90, 90),
+ new Coordinate(50, 10),
+ new Coordinate(10, 90)
+ };
+
+ // Compare the transformed geometry with the expected geometry
+ LinearRing expectedShell = geometryFactory.createLinearRing(expectedCoordinates);
+ Polygon expectedGeom = geometryFactory.createPolygon(expectedShell);
+ assertTrue(outputGeom.equalsTopo(expectedGeom));
+
+ }
+}