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/10/27 10:11:53 UTC

(incubator-baremaps) 06/11: Refactor package and introduce a PMTilesReader class

This is an automated email from the ASF dual-hosted git repository.

bchapuis pushed a commit to branch pmtiles
in repository https://gitbox.apache.org/repos/asf/incubator-baremaps.git

commit 8c780a06aae5597383a5eaf4b70613216e60890b
Author: Bertil Chapuis <bc...@gmail.com>
AuthorDate: Thu Sep 7 17:25:58 2023 +0200

    Refactor package and introduce a PMTilesReader class
---
 .../baremaps/tilestore/pmtiles/Compression.java    |  70 ++++++
 .../apache/baremaps/tilestore/pmtiles/Entry.java   |  51 ++++
 .../apache/baremaps/tilestore/pmtiles/Header.java  | 258 +++++++++++++++++++++
 .../apache/baremaps/tilestore/pmtiles/PMTiles.java | 160 +++----------
 .../baremaps/tilestore/pmtiles/TileType.java       |  10 +
 .../baremaps/tilestore/pmtiles/PMTilesReader.java  |  77 ++++++
 .../baremaps/tilestore/pmtiles/PMTilesTest.java    |  84 ++++---
 .../baremaps/vectortile/VectorTileViewer.java      |   2 +-
 8 files changed, 554 insertions(+), 158 deletions(-)

diff --git a/baremaps-core/src/main/java/org/apache/baremaps/tilestore/pmtiles/Compression.java b/baremaps-core/src/main/java/org/apache/baremaps/tilestore/pmtiles/Compression.java
new file mode 100644
index 00000000..4360d9c7
--- /dev/null
+++ b/baremaps-core/src/main/java/org/apache/baremaps/tilestore/pmtiles/Compression.java
@@ -0,0 +1,70 @@
+package org.apache.baremaps.tilestore.pmtiles;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+enum Compression {
+    Unknown,
+    None,
+    Gzip,
+    Brotli,
+    Zstd;
+
+    ByteBuffer decompress(ByteBuffer buffer) {
+        return switch (this) {
+            case None -> buffer;
+            case Gzip -> decompressGzip(buffer);
+            case Brotli -> decompressBrotli(buffer);
+            case Zstd -> decompressZstd(buffer);
+            default -> throw new RuntimeException("Unknown compression");
+        };
+    }
+
+    static ByteBuffer decompressGzip(ByteBuffer buffer) {
+        try(var inputStream = new GZIPInputStream(new ByteArrayInputStream(buffer.array()))) {
+            return ByteBuffer.wrap(inputStream.readAllBytes());
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    static ByteBuffer decompressBrotli(ByteBuffer buffer) {
+        throw new RuntimeException("Brotli compression not implemented");
+    }
+
+    static ByteBuffer decompressZstd(ByteBuffer buffer) {
+        throw new RuntimeException("Zstd compression not implemented");
+    }
+
+    ByteBuffer compress(ByteBuffer buffer) {
+        return switch (this) {
+            case None -> buffer;
+            case Gzip -> compressGzip(buffer);
+            case Brotli -> compressBrotli(buffer);
+            case Zstd -> compressZstd(buffer);
+            default -> throw new RuntimeException("Unknown compression");
+        };
+    }
+
+    static ByteBuffer compressGzip(ByteBuffer buffer) {
+        try(var outputStream = new ByteArrayOutputStream();
+        var gzipOutputStream = new GZIPOutputStream(outputStream)) {
+            gzipOutputStream.write(buffer.array());
+            return ByteBuffer.wrap(outputStream.toByteArray());
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    static ByteBuffer compressBrotli(ByteBuffer buffer) {
+        throw new RuntimeException("Brotli compression not implemented");
+    }
+
+    static ByteBuffer compressZstd(ByteBuffer buffer) {
+        throw new RuntimeException("Zstd compression not implemented");
+    }
+}
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/tilestore/pmtiles/Entry.java b/baremaps-core/src/main/java/org/apache/baremaps/tilestore/pmtiles/Entry.java
new file mode 100644
index 00000000..97fe00e2
--- /dev/null
+++ b/baremaps-core/src/main/java/org/apache/baremaps/tilestore/pmtiles/Entry.java
@@ -0,0 +1,51 @@
+package org.apache.baremaps.tilestore.pmtiles;
+
+public class Entry {
+    private long tileId;
+    private long offset;
+    private long length;
+    private long runLength;
+
+    public Entry() {
+
+    }
+
+    public Entry(long tileId, long offset, long length, long runLength) {
+        this.tileId = tileId;
+        this.offset = offset;
+        this.length = length;
+        this.runLength = runLength;
+    }
+
+    public long getTileId() {
+        return tileId;
+    }
+
+    public void setTileId(long tileId) {
+        this.tileId = tileId;
+    }
+
+    public long getOffset() {
+        return offset;
+    }
+
+    public void setOffset(long offset) {
+        this.offset = offset;
+    }
+
+    public long getLength() {
+        return length;
+    }
+
+    public void setLength(long length) {
+        this.length = length;
+    }
+
+    public long getRunLength() {
+        return runLength;
+    }
+
+    public void setRunLength(long runLength) {
+        this.runLength = runLength;
+    }
+}
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/tilestore/pmtiles/Header.java b/baremaps-core/src/main/java/org/apache/baremaps/tilestore/pmtiles/Header.java
new file mode 100644
index 00000000..d3cb42cb
--- /dev/null
+++ b/baremaps-core/src/main/java/org/apache/baremaps/tilestore/pmtiles/Header.java
@@ -0,0 +1,258 @@
+package org.apache.baremaps.tilestore.pmtiles;
+
+public class Header {
+
+    private int specVersion;
+    private long rootDirectoryOffset;
+    private long rootDirectoryLength;
+    private long jsonMetadataOffset;
+    private long jsonMetadataLength;
+    private long leafDirectoryOffset;
+    private long leafDirectoryLength;
+    private long tileDataOffset;
+    private long tileDataLength;
+    private long numAddressedTiles;
+    private long numTileEntries;
+    private long numTileContents;
+    private boolean clustered;
+    private Compression internalCompression;
+    private Compression tileCompression;
+    private TileType tileType;
+    private int minZoom;
+    private int maxZoom;
+    private double minLon;
+    private double minLat;
+    private double maxLon;
+    private double maxLat;
+    private int centerZoom;
+    private double centerLon;
+    private double centerLat;
+
+    public Header(int specVersion, long rootDirectoryOffset, long rootDirectoryLength, long jsonMetadataOffset, long jsonMetadataLength, long leafDirectoryOffset, long leafDirectoryLength, long tileDataOffset, long tileDataLength, long numAddressedTiles, long numTileEntries, long numTileContents, boolean clustered, Compression internalCompression, Compression tileCompression, TileType tileType, int minZoom, int maxZoom, double minLon, double minLat, double maxLon, double maxLat, int cent [...]
+        this.specVersion = specVersion;
+        this.rootDirectoryOffset = rootDirectoryOffset;
+        this.rootDirectoryLength = rootDirectoryLength;
+        this.jsonMetadataOffset = jsonMetadataOffset;
+        this.jsonMetadataLength = jsonMetadataLength;
+        this.leafDirectoryOffset = leafDirectoryOffset;
+        this.leafDirectoryLength = leafDirectoryLength;
+        this.tileDataOffset = tileDataOffset;
+        this.tileDataLength = tileDataLength;
+        this.numAddressedTiles = numAddressedTiles;
+        this.numTileEntries = numTileEntries;
+        this.numTileContents = numTileContents;
+        this.clustered = clustered;
+        this.internalCompression = internalCompression;
+        this.tileCompression = tileCompression;
+        this.tileType = tileType;
+        this.minZoom = minZoom;
+        this.maxZoom = maxZoom;
+        this.minLon = minLon;
+        this.minLat = minLat;
+        this.maxLon = maxLon;
+        this.maxLat = maxLat;
+        this.centerZoom = centerZoom;
+        this.centerLon = centerLon;
+        this.centerLat = centerLat;
+    }
+
+    public int getSpecVersion() {
+        return specVersion;
+    }
+
+    public void setSpecVersion(int specVersion) {
+        this.specVersion = specVersion;
+    }
+
+    public long getRootDirectoryOffset() {
+        return rootDirectoryOffset;
+    }
+
+    public void setRootDirectoryOffset(long rootDirectoryOffset) {
+        this.rootDirectoryOffset = rootDirectoryOffset;
+    }
+
+    public long getRootDirectoryLength() {
+        return rootDirectoryLength;
+    }
+
+    public void setRootDirectoryLength(long rootDirectoryLength) {
+        this.rootDirectoryLength = rootDirectoryLength;
+    }
+
+    public long getJsonMetadataOffset() {
+        return jsonMetadataOffset;
+    }
+
+    public void setJsonMetadataOffset(long jsonMetadataOffset) {
+        this.jsonMetadataOffset = jsonMetadataOffset;
+    }
+
+    public long getJsonMetadataLength() {
+        return jsonMetadataLength;
+    }
+
+    public void setJsonMetadataLength(long jsonMetadataLength) {
+        this.jsonMetadataLength = jsonMetadataLength;
+    }
+
+    public long getLeafDirectoryOffset() {
+        return leafDirectoryOffset;
+    }
+
+    public void setLeafDirectoryOffset(long leafDirectoryOffset) {
+        this.leafDirectoryOffset = leafDirectoryOffset;
+    }
+
+    public long getLeafDirectoryLength() {
+        return leafDirectoryLength;
+    }
+
+    public void setLeafDirectoryLength(long leafDirectoryLength) {
+        this.leafDirectoryLength = leafDirectoryLength;
+    }
+
+    public long getTileDataOffset() {
+        return tileDataOffset;
+    }
+
+    public void setTileDataOffset(long tileDataOffset) {
+        this.tileDataOffset = tileDataOffset;
+    }
+
+    public long getTileDataLength() {
+        return tileDataLength;
+    }
+
+    public void setTileDataLength(long tileDataLength) {
+        this.tileDataLength = tileDataLength;
+    }
+
+    public long getNumAddressedTiles() {
+        return numAddressedTiles;
+    }
+
+    public void setNumAddressedTiles(long numAddressedTiles) {
+        this.numAddressedTiles = numAddressedTiles;
+    }
+
+    public long getNumTileEntries() {
+        return numTileEntries;
+    }
+
+    public void setNumTileEntries(long numTileEntries) {
+        this.numTileEntries = numTileEntries;
+    }
+
+    public long getNumTileContents() {
+        return numTileContents;
+    }
+
+    public void setNumTileContents(long numTileContents) {
+        this.numTileContents = numTileContents;
+    }
+
+    public boolean isClustered() {
+        return clustered;
+    }
+
+    public void setClustered(boolean clustered) {
+        this.clustered = clustered;
+    }
+
+    public Compression getInternalCompression() {
+        return internalCompression;
+    }
+
+    public void setInternalCompression(Compression internalCompression) {
+        this.internalCompression = internalCompression;
+    }
+
+    public Compression getTileCompression() {
+        return tileCompression;
+    }
+
+    public void setTileCompression(Compression tileCompression) {
+        this.tileCompression = tileCompression;
+    }
+
+    public TileType getTileType() {
+        return tileType;
+    }
+
+    public void setTileType(TileType tileType) {
+        this.tileType = tileType;
+    }
+
+    public int getMinZoom() {
+        return minZoom;
+    }
+
+    public void setMinZoom(int minZoom) {
+        this.minZoom = minZoom;
+    }
+
+    public int getMaxZoom() {
+        return maxZoom;
+    }
+
+    public void setMaxZoom(int maxZoom) {
+        this.maxZoom = maxZoom;
+    }
+
+    public double getMinLon() {
+        return minLon;
+    }
+
+    public void setMinLon(double minLon) {
+        this.minLon = minLon;
+    }
+
+    public double getMinLat() {
+        return minLat;
+    }
+
+    public void setMinLat(double minLat) {
+        this.minLat = minLat;
+    }
+
+    public double getMaxLon() {
+        return maxLon;
+    }
+
+    public void setMaxLon(double maxLon) {
+        this.maxLon = maxLon;
+    }
+
+    public double getMaxLat() {
+        return maxLat;
+    }
+
+    public void setMaxLat(double maxLat) {
+        this.maxLat = maxLat;
+    }
+
+    public int getCenterZoom() {
+        return centerZoom;
+    }
+
+    public void setCenterZoom(int centerZoom) {
+        this.centerZoom = centerZoom;
+    }
+
+    public double getCenterLon() {
+        return centerLon;
+    }
+
+    public void setCenterLon(double centerLon) {
+        this.centerLon = centerLon;
+    }
+
+    public double getCenterLat() {
+        return centerLat;
+    }
+
+    public void setCenterLat(double centerLat) {
+        this.centerLat = centerLat;
+    }
+}
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/tilestore/pmtiles/PMTiles.java b/baremaps-core/src/main/java/org/apache/baremaps/tilestore/pmtiles/PMTiles.java
index 1ff45353..7d260a5b 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/tilestore/pmtiles/PMTiles.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/tilestore/pmtiles/PMTiles.java
@@ -168,55 +168,9 @@ public class PMTiles {
     throw new RuntimeException("Tile zoom level exceeds max safe number limit (26)");
   }
 
-  enum Compression {
-    Unknown,
-    None,
-    Gzip,
-    Brotli,
-    Zstd,
-  }
-
-  enum TileType {
-    Unknown,
-    Mvt,
-    Png,
-    Jpeg,
-    Webp,
-    Avif,
-  }
-
   private static final int HEADER_SIZE_BYTES = 127;
 
-  public record Header(
-      int specVersion,
-      long rootDirectoryOffset,
-      long rootDirectoryLength,
-      long jsonMetadataOffset,
-      long jsonMetadataLength,
-      long leafDirectoryOffset,
-      long leafDirectoryLength,
-      long tileDataOffset,
-      long tileDataLength,
-      long numAddressedTiles,
-      long numTileEntries,
-      long numTileContents,
-      boolean clustered,
-      Compression internalCompression,
-      Compression tileCompression,
-      TileType tileType,
-      int minZoom,
-      int maxZoom,
-      double minLon,
-      double minLat,
-      double maxLon,
-      double maxLat,
-      int centerZoom,
-      double centerLon,
-      double centerLat,
-      String etag) {
-  }
-
-  public static Header decodeHeader(ByteBuffer buf, String etag) {
+  public static Header decodeHeader(ByteBuffer buf) {
     buf.order(ByteOrder.LITTLE_ENDIAN);
     return new Header(
         buf.get(7),
@@ -243,8 +197,8 @@ public class PMTiles {
         (double) buf.getInt(114) / 10000000,
         buf.get(118),
         (double) buf.getInt(119) / 10000000,
-        (double) buf.getInt(123) / 10000000,
-        etag);
+        (double) buf.getInt(123) / 10000000
+    );
   }
 
   public static void encodeHeader(Header header, ByteBuffer buf) {
@@ -256,81 +210,31 @@ public class PMTiles {
     buf.put(4, (byte) 0x6C);
     buf.put(5, (byte) 0x65);
     buf.put(6, (byte) 0x73);
-    buf.put(7, (byte) header.specVersion);
-    buf.putLong(8, header.rootDirectoryOffset);
-    buf.putLong(16, header.rootDirectoryLength);
-    buf.putLong(24, header.jsonMetadataOffset);
-    buf.putLong(32, header.jsonMetadataLength);
-    buf.putLong(40, header.leafDirectoryOffset);
-    buf.putLong(48, header.leafDirectoryLength);
-    buf.putLong(56, header.tileDataOffset);
-    buf.putLong(64, header.tileDataLength);
-    buf.putLong(72, header.numAddressedTiles);
-    buf.putLong(80, header.numTileEntries);
-    buf.putLong(88, header.numTileContents);
-    buf.put(96, (byte) (header.clustered ? 1 : 0));
-    buf.put(97, (byte) header.internalCompression.ordinal());
-    buf.put(98, (byte) header.tileCompression.ordinal());
-    buf.put(99, (byte) header.tileType.ordinal());
-    buf.put(100, (byte) header.minZoom);
-    buf.put(101, (byte) header.maxZoom);
-    buf.putInt(102, (int) (header.minLon * 10000000));
-    buf.putInt(106, (int) (header.minLat * 10000000));
-    buf.putInt(110, (int) (header.maxLon * 10000000));
-    buf.putInt(114, (int) (header.maxLat * 10000000));
-    buf.put(118, (byte) header.centerZoom);
-    buf.putInt(119, (int) (header.centerLon * 10000000));
-    buf.putInt(123, (int) (header.centerLat * 10000000));
-  }
-
-  public static class Entry {
-    private long tileId;
-    private long offset;
-    private long length;
-    private long runLength;
-
-    public Entry() {
-
-    }
-
-    public Entry(long tileId, long offset, long length, long runLength) {
-      this.tileId = tileId;
-      this.offset = offset;
-      this.length = length;
-      this.runLength = runLength;
-    }
-
-    public long getTileId() {
-      return tileId;
-    }
-
-    public void setTileId(long tileId) {
-      this.tileId = tileId;
-    }
-
-    public long getOffset() {
-      return offset;
-    }
-
-    public void setOffset(long offset) {
-      this.offset = offset;
-    }
-
-    public long getLength() {
-      return length;
-    }
-
-    public void setLength(long length) {
-      this.length = length;
-    }
-
-    public long getRunLength() {
-      return runLength;
-    }
-
-    public void setRunLength(long runLength) {
-      this.runLength = runLength;
-    }
+    buf.put(7, (byte) header.getSpecVersion());
+    buf.putLong(8, header.getRootDirectoryOffset());
+    buf.putLong(16, header.getRootDirectoryLength());
+    buf.putLong(24, header.getJsonMetadataOffset());
+    buf.putLong(32, header.getJsonMetadataLength());
+    buf.putLong(40, header.getLeafDirectoryOffset());
+    buf.putLong(48, header.getLeafDirectoryLength());
+    buf.putLong(56, header.getTileDataOffset());
+    buf.putLong(64, header.getTileDataLength());
+    buf.putLong(72, header.getNumAddressedTiles());
+    buf.putLong(80, header.getNumTileEntries());
+    buf.putLong(88, header.getNumTileContents());
+    buf.put(96, (byte) (header.isClustered() ? 1 : 0));
+    buf.put(97, (byte) header.getInternalCompression().ordinal());
+    buf.put(98, (byte) header.getTileCompression().ordinal());
+    buf.put(99, (byte) header.getTileType().ordinal());
+    buf.put(100, (byte) header.getMinZoom());
+    buf.put(101, (byte) header.getMaxZoom());
+    buf.putInt(102, (int) (header.getMinLon() * 10000000));
+    buf.putInt(106, (int) (header.getMinLat() * 10000000));
+    buf.putInt(110, (int) (header.getMaxLon() * 10000000));
+    buf.putInt(114, (int) (header.getMaxLat() * 10000000));
+    buf.put(118, (byte) header.getCenterZoom());
+    buf.putInt(119, (int) (header.getCenterLon() * 10000000));
+    buf.putInt(123, (int) (header.getCenterLat() * 10000000));
   }
 
   public static void encodeDirectory(ByteBuffer buffer, List<Entry> entries) {
@@ -350,7 +254,7 @@ public class PMTiles {
     for (Entry entry : entries) {
       if (entry.getOffset() == 0 && entry.getLength() > 0) {
         Entry prevEntry = entries.get(entries.indexOf(entry) - 1);
-        encodeVarInt(buffer, prevEntry.offset + prevEntry.length + 1);
+        encodeVarInt(buffer, prevEntry.getOffset() + prevEntry.getLength() + 1);
       } else {
         encodeVarInt(buffer, entry.getOffset() + 1);
       }
@@ -381,7 +285,7 @@ public class PMTiles {
       long value = decodeVarInt(buffer);
       if (value == 0 && i > 0) {
         Entry prevEntry = entries.get(i - 1);
-        entries.get(i).setOffset(prevEntry.offset + prevEntry.length);;
+        entries.get(i).setOffset(prevEntry.getOffset() + prevEntry.getLength());;
       } else {
         entries.get(i).setOffset(value - 1);
       }
@@ -406,10 +310,10 @@ public class PMTiles {
 
     // at this point, m > n
     if (n >= 0) {
-      if (entries.get(n).runLength == 0) {
+      if (entries.get(n).getRunLength() == 0) {
         return entries.get(n);
       }
-      if (tileId - entries.get(n).tileId < entries.get(n).runLength) {
+      if (tileId - entries.get(n).getTileId() < entries.get(n).getRunLength()) {
         return entries.get(n);
       }
     }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/tilestore/pmtiles/TileType.java b/baremaps-core/src/main/java/org/apache/baremaps/tilestore/pmtiles/TileType.java
new file mode 100644
index 00000000..10bf41c8
--- /dev/null
+++ b/baremaps-core/src/main/java/org/apache/baremaps/tilestore/pmtiles/TileType.java
@@ -0,0 +1,10 @@
+package org.apache.baremaps.tilestore.pmtiles;
+
+enum TileType {
+    Unknown,
+    Mvt,
+    Png,
+    Jpeg,
+    Webp,
+    Avif,
+}
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/tilestore/pmtiles/PMTilesReader.java b/baremaps-core/src/test/java/org/apache/baremaps/tilestore/pmtiles/PMTilesReader.java
new file mode 100644
index 00000000..4ca92112
--- /dev/null
+++ b/baremaps-core/src/test/java/org/apache/baremaps/tilestore/pmtiles/PMTilesReader.java
@@ -0,0 +1,77 @@
+package org.apache.baremaps.tilestore.pmtiles;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+public class PMTilesReader {
+
+    private final Path path;
+
+    private Header header;
+
+    private List<Entry> rootEntries;
+
+    public PMTilesReader(Path path) {
+        this.path = path;
+    }
+
+    public Header getHeader() {
+        if (header == null) {
+            try (var channel = Files.newByteChannel(path)) {
+                var buffer = ByteBuffer.allocate(127);
+                channel.read(buffer);
+                buffer.flip();
+                header = PMTiles.decodeHeader(buffer);
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+        return header;
+    }
+
+    public List<Entry> getRootDirectory() {
+        if (rootEntries == null) {
+            var header = getHeader();
+            rootEntries = getDirectory(header.getRootDirectoryOffset(), (int) header.getRootDirectoryLength());
+        }
+        return rootEntries;
+    }
+
+    public List<Entry> getDirectory(long offset, int length) {
+        var header = getHeader();
+        try (var channel = Files.newByteChannel(path)) {
+            var compressed = ByteBuffer.allocate(length);
+            channel.position(offset);
+            channel.read(compressed);
+            compressed.flip();
+            var decompressed = header.getInternalCompression().decompress(compressed);
+            return PMTiles.decodeDirectory(decompressed);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public ByteBuffer getTile(int z, long x, long y) {
+        var tileId = PMTiles.zxyToTileId(z, x, y);
+        var header = getHeader();
+        var entries = getRootDirectory();
+        var entry = PMTiles.findTile(entries, tileId);
+
+        if (entry == null) {
+            return null;
+        }
+
+        try (var channel = Files.newByteChannel(path)) {
+            var compressed = ByteBuffer.allocate((int) entry.getLength());
+            channel.position(header.getTileDataOffset() + entry.getOffset());
+            channel.read(compressed);
+            compressed.flip();
+            return header.getTileCompression().decompress(compressed);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/tilestore/pmtiles/PMTilesTest.java b/baremaps-core/src/test/java/org/apache/baremaps/tilestore/pmtiles/PMTilesTest.java
index 12adeaac..84211805 100644
--- a/baremaps-core/src/test/java/org/apache/baremaps/tilestore/pmtiles/PMTilesTest.java
+++ b/baremaps-core/src/test/java/org/apache/baremaps/tilestore/pmtiles/PMTilesTest.java
@@ -19,12 +19,16 @@ import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.file.Files;
 import java.util.ArrayList;
+
+import com.google.protobuf.InvalidProtocolBufferException;
 import org.apache.baremaps.testing.TestFiles;
-import org.apache.baremaps.tilestore.pmtiles.PMTiles.Compression;
-import org.apache.baremaps.tilestore.pmtiles.PMTiles.Entry;
-import org.apache.baremaps.tilestore.pmtiles.PMTiles.TileType;
+import org.apache.baremaps.vectortile.VectorTileDecoder;
+import org.apache.baremaps.vectortile.VectorTileViewer;
+import org.apache.baremaps.vectortile.VectorTileViewer.TilePanel;
 import org.junit.jupiter.api.Test;
 
+import javax.swing.*;
+
 class PMTilesTest {
 
   @Test
@@ -127,28 +131,28 @@ class PMTilesTest {
       var buffer = ByteBuffer.allocate(127);
       channel.read(buffer);
       buffer.flip();
-      var header = PMTiles.decodeHeader(buffer, "1");
-      assertEquals(header.rootDirectoryOffset(), 127);
-      assertEquals(header.rootDirectoryLength(), 25);
-      assertEquals(header.jsonMetadataOffset(), 152);
-      assertEquals(header.jsonMetadataLength(), 247);
-      assertEquals(header.leafDirectoryOffset(), 0);
-      assertEquals(header.leafDirectoryLength(), 0);
-      assertEquals(header.tileDataOffset(), 399);
-      assertEquals(header.tileDataLength(), 69);
-      assertEquals(header.numAddressedTiles(), 1);
-      assertEquals(header.numTileEntries(), 1);
-      assertEquals(header.numTileContents(), 1);
-      assertFalse(header.clustered());
-      assertEquals(header.internalCompression(), Compression.Gzip);
-      assertEquals(header.tileCompression(), Compression.Gzip);
-      assertEquals(header.tileType(), TileType.Mvt);
-      assertEquals(header.minZoom(), 0);
-      assertEquals(header.maxZoom(), 0);
-      assertEquals(header.minLon(), 0);
-      assertEquals(header.minLat(), 0);
-      assertEquals(Math.round(header.maxLon()), 1);
-      assertEquals(Math.round(header.maxLat()), 1);
+      var header = PMTiles.decodeHeader(buffer);
+      assertEquals(header.getRootDirectoryOffset(), 127);
+      assertEquals(header.getRootDirectoryLength(), 25);
+      assertEquals(header.getJsonMetadataOffset(), 152);
+      assertEquals(header.getJsonMetadataLength(), 247);
+      assertEquals(header.getLeafDirectoryOffset(), 0);
+      assertEquals(header.getLeafDirectoryLength(), 0);
+      assertEquals(header.getTileDataOffset(), 399);
+      assertEquals(header.getTileDataLength(), 69);
+      assertEquals(header.getNumAddressedTiles(), 1);
+      assertEquals(header.getNumTileEntries(), 1);
+      assertEquals(header.getNumTileContents(), 1);
+      assertFalse(header.isClustered());
+      assertEquals(header.getInternalCompression(), Compression.Gzip);
+      assertEquals(header.getTileCompression(), Compression.Gzip);
+      assertEquals(header.getTileType(), TileType.Mvt);
+      assertEquals(header.getMinZoom(), 0);
+      assertEquals(header.getMaxZoom(), 0);
+      assertEquals(header.getMinLon(), 0);
+      assertEquals(header.getMinLat(), 0);
+      assertEquals(Math.round(header.getMaxLon()), 1);
+      assertEquals(Math.round(header.getMaxLat()), 1);
     }
   }
 
@@ -156,7 +160,7 @@ class PMTilesTest {
   void encodeHeader() throws IOException {
     var etag = "1";
     var buffer = ByteBuffer.allocate(127);
-    var header = new PMTiles.Header(
+    var header = new Header(
         127,
         25,
         152,
@@ -181,10 +185,10 @@ class PMTilesTest {
         0,
         0,
         0,
-        0,
-        etag);
+        0
+    );
     PMTiles.encodeHeader(header, buffer);
-    var header2 = PMTiles.decodeHeader(buffer, etag);
+    var header2 = PMTiles.decodeHeader(buffer);
     assertEquals(header, header2);
   }
 
@@ -244,5 +248,27 @@ class PMTilesTest {
     assertEquals(entry.getLength(), 1);
   }
 
+  @Test
+  void reader() throws InvalidProtocolBufferException, InterruptedException {
+    var reader = new PMTilesReader(TestFiles.resolve("pmtiles/test_fixture_1.pmtiles"));
+    var header = reader.getHeader();
+    assertEquals(header.getRootDirectoryOffset(), 127);
+    var rootDirectory = reader.getRootDirectory();
+    assertEquals(rootDirectory.size(), 1);
+    var entry = rootDirectory.get(0);
+    ByteBuffer buffer = reader.getTile(0,0,0);
+
+    VectorTileViewer viewer = new VectorTileViewer();
+    var parsed = org.apache.baremaps.mvt.binary.VectorTile.Tile.parseFrom(buffer);
+    var tile = new VectorTileDecoder().decodeTile(parsed);
+    JFrame f = new JFrame("Vector Tile Viewer");
+    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+    f.add(new TilePanel(tile, 1000));
+    f.pack();
+    f.setVisible(true);
+
+    Thread.sleep(10000);
+  }
+
 
 }
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/vectortile/VectorTileViewer.java b/baremaps-core/src/test/java/org/apache/baremaps/vectortile/VectorTileViewer.java
index faada968..36e925f4 100644
--- a/baremaps-core/src/test/java/org/apache/baremaps/vectortile/VectorTileViewer.java
+++ b/baremaps-core/src/test/java/org/apache/baremaps/vectortile/VectorTileViewer.java
@@ -49,7 +49,7 @@ public class VectorTileViewer {
     }
   }
 
-  static class TilePanel extends JPanel {
+  public static class TilePanel extends JPanel {
 
     private final Tile tile;