planetiler/planetiler-core/src/main/java/com/onthegomap/planetiler/pmtiles/ReadablePmtiles.java

206 wiersze
6.3 KiB
Java

package com.onthegomap.planetiler.pmtiles;
import com.onthegomap.planetiler.archive.ReadableTileArchive;
import com.onthegomap.planetiler.archive.TileArchiveMetadata;
import com.onthegomap.planetiler.geo.TileCoord;
import com.onthegomap.planetiler.util.CloseableIterator;
import com.onthegomap.planetiler.util.Gzip;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class ReadablePmtiles implements ReadableTileArchive {
private final SeekableByteChannel channel;
private final Pmtiles.Header header;
public ReadablePmtiles(SeekableByteChannel channel) throws IOException {
this.channel = channel;
this.header = Pmtiles.Header.fromBytes(getBytes(0, Pmtiles.HEADER_LEN));
}
public static ReadableTileArchive newReadFromFile(Path path) throws IOException {
return new ReadablePmtiles(FileChannel.open(path, StandardOpenOption.READ));
}
private synchronized byte[] getBytes(long start, int length) throws IOException {
channel.position(start);
var buf = ByteBuffer.allocate(length);
channel.read(buf);
return buf.array();
}
/**
* Finds the relevant entry for a tileId in a list of entries.
* <p>
* If there is an exact match for tileId, return that. Else if the tileId matches an entry's tileId + runLength,
* return that. Else if the preceding entry is a directory (runLength = 0), return that. Else return null.
*/
public static Pmtiles.Entry findTile(List<Pmtiles.Entry> entries, long tileId) {
int m = 0;
int n = entries.size() - 1;
while (m <= n) {
int k = (n + m) >> 1;
long cmp = tileId - entries.get(k).tileId();
if (cmp > 0) {
m = k + 1;
} else if (cmp < 0) {
n = k - 1;
} else {
return entries.get(k);
}
}
if (n >= 0 && (entries.get(n).runLength() == 0 || tileId - entries.get(n).tileId() < entries.get(n).runLength())) {
return entries.get(n);
}
return null;
}
@Override
@SuppressWarnings("java:S1168")
public byte[] getTile(int x, int y, int z) {
try {
var tileId = TileCoord.ofXYZ(x, y, z).hilbertEncoded();
long dirOffset = header.rootDirOffset();
int dirLength = (int) header.rootDirLength();
for (int depth = 0; depth <= 3; depth++) {
byte[] dirBytes = getBytes(dirOffset, dirLength);
if (header.internalCompression() == Pmtiles.Compression.GZIP) {
dirBytes = Gzip.gunzip(dirBytes);
}
var dir = Pmtiles.directoryFromBytes(dirBytes);
var entry = findTile(dir, tileId);
if (entry != null) {
if (entry.runLength() > 0) {
return getBytes(header.tileDataOffset() + entry.offset(), entry.length());
} else {
dirOffset = header.leafDirectoriesOffset() + entry.offset();
dirLength = entry.length();
}
} else {
return null;
}
}
} catch (IOException e) {
throw new IllegalStateException("Could not get tile", e);
}
return null;
}
public Pmtiles.Header getHeader() {
return header;
}
public Pmtiles.JsonMetadata getJsonMetadata() throws IOException {
var buf = getBytes(header.jsonMetadataOffset(), (int) header.jsonMetadataLength());
if (header.internalCompression() == Pmtiles.Compression.GZIP) {
buf = Gzip.gunzip(buf);
}
return Pmtiles.JsonMetadata.fromBytes(buf);
}
@Override
public TileArchiveMetadata metadata() {
try {
var jsonMetadata = getJsonMetadata();
var map = new LinkedHashMap<>(jsonMetadata.otherMetadata());
return new TileArchiveMetadata(
map.remove(TileArchiveMetadata.NAME_KEY),
map.remove(TileArchiveMetadata.DESCRIPTION_KEY),
map.remove(TileArchiveMetadata.ATTRIBUTION_KEY),
map.remove(TileArchiveMetadata.VERSION_KEY),
map.remove(TileArchiveMetadata.TYPE_KEY),
switch (header.tileType()) {
case MVT -> TileArchiveMetadata.MVT_FORMAT;
default -> null;
},
header.bounds(),
header.center(),
(double) header.centerZoom(),
(int) header.minZoom(),
(int) header.maxZoom(),
jsonMetadata.vectorLayers(),
map
);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static class TileCoordIterator implements CloseableIterator<TileCoord> {
private final Stream<TileCoord> stream;
private final Iterator<TileCoord> iterator;
public TileCoordIterator(Stream<TileCoord> stream) {
this.stream = stream;
this.iterator = stream.iterator();
}
@Override
public void close() {
stream.close();
}
@Override
public boolean hasNext() {
return this.iterator.hasNext();
}
@Override
public TileCoord next() {
return this.iterator.next();
}
}
private List<Pmtiles.Entry> readDir(long offset, int length) throws IOException {
var buf = getBytes(offset, length);
if (header.internalCompression() == Pmtiles.Compression.GZIP) {
buf = Gzip.gunzip(buf);
}
return Pmtiles.directoryFromBytes(buf);
}
// Warning: this will only work on z15 or less pmtiles which planetiler creates
private Stream<TileCoord> getTileCoords(List<Pmtiles.Entry> dir) {
return dir.stream().flatMap(entry -> {
try {
return entry.runLength() == 0 ?
getTileCoords(readDir(header.leafDirectoriesOffset() + entry.offset(), entry.length())) : IntStream
.range((int) entry.tileId(), (int) entry.tileId() + entry.runLength()).mapToObj(TileCoord::hilbertDecode);
} catch (IOException e) {
throw new IllegalStateException(e);
}
});
}
@Override
public CloseableIterator<TileCoord> getAllTileCoords() {
List<Pmtiles.Entry> rootDir;
try {
rootDir = readDir(header.rootDirOffset(), (int) header.rootDirLength());
return new TileCoordIterator(getTileCoords(rootDir));
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
@Override
public void close() throws IOException {
channel.close();
}
}