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));
+
+  }
+}