Skip to content

Commit

Permalink
Add support for "files"-archive
Browse files Browse the repository at this point in the history
i.e. write individual pbf-files to disk in the format <base>/z/x/y.pbf

in order to use that format it must be passed as "--ouput=/path/to/tiles?format=files"

Fixes #536
  • Loading branch information
bbilger committed Dec 20, 2023
1 parent 401adf4 commit 6e8effd
Show file tree
Hide file tree
Showing 10 changed files with 532 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import com.onthegomap.planetiler.stats.ProcessInfo;
import com.onthegomap.planetiler.stats.Stats;
import com.onthegomap.planetiler.stats.Timers;
import com.onthegomap.planetiler.stream.StreamArchiveUtils;
import com.onthegomap.planetiler.util.AnsiColors;
import com.onthegomap.planetiler.util.BuildInfo;
import com.onthegomap.planetiler.util.ByteBufferUtil;
Expand Down Expand Up @@ -683,15 +682,15 @@ public void run() throws Exception {
throw new IllegalArgumentException(output.format() + " doesn't support concurrent writes");
}
IntStream.range(1, config.tileWriteThreads())
.mapToObj(index -> StreamArchiveUtils.constructIndexedPath(output.getLocalPath(), index))
.mapToObj(output::getPathForMultiThreadedWriter)
.forEach(p -> {
if (!config.append() && (overwrite || config.force())) {
FileUtils.delete(p);
}
if (config.append() && !Files.exists(p)) {
throw new IllegalArgumentException("indexed file \"" + p + "\" must exist when appending");
} else if (!config.append() && Files.exists(p)) {
throw new IllegalArgumentException("indexed file \"" + p + "\" must not exist when not appending");
if (config.append() && !output.exists(p)) {
throw new IllegalArgumentException("indexed archive \"" + p + "\" must exist when appending");
} else if (!config.append() && output.exists(p)) {
throw new IllegalArgumentException("indexed archive \"" + p + "\" must not exist when not appending");
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
import static com.onthegomap.planetiler.util.LanguageUtils.nullIfEmpty;

import com.onthegomap.planetiler.config.Arguments;
import com.onthegomap.planetiler.stream.StreamArchiveUtils;
import com.onthegomap.planetiler.util.FileUtils;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;

/**
* Definition for a tileset, parsed from a URI-like string.
Expand Down Expand Up @@ -147,7 +151,30 @@ public void delete() {
* Returns {@code true} if the archive already exists, {@code false} otherwise.
*/
public boolean exists() {
return getLocalPath() != null && Files.exists(getLocalPath());
return exists(getLocalPath());
}

/**
* @param p path to the archive
* @return {@code true} if the archive already exists, {@code false} otherwise.
*/
public boolean exists(Path p) {
if (p == null) {
return false;
}
if (format() != Format.FILES) {
return Files.exists(p);
} else {
if (!Files.exists(p)) {
return false;
}
// file-archive exists only if it has any contents
try (Stream<Path> paths = Files.list(p)) {
return paths.findAny().isPresent();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}

/**
Expand All @@ -165,6 +192,14 @@ public Arguments applyFallbacks(Arguments arguments) {
return Arguments.of(options).orElse(arguments.withPrefix(format.id));
}

public Path getPathForMultiThreadedWriter(int index) {
return switch (format) {
case CSV, TSV, JSON, PROTO, PBF -> StreamArchiveUtils.constructIndexedPath(getLocalPath(), index);
case FILES -> getLocalPath();
default -> throw new UnsupportedOperationException("not supported by " + format);
};
}

public enum Format {
MBTILES("mbtiles",
false /* TODO mbtiles could support append in the future by using insert statements with an "on conflict"-clause (i.e. upsert) and by creating tables only if they don't exist, yet */,
Expand All @@ -179,7 +214,9 @@ public enum Format {
/** identical to {@link Format#PROTO} */
PBF("pbf", true, true),

JSON("json", true, true);
JSON("json", true, true),

FILES("files", true, true);

private final String id;
private final boolean supportsAppend;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.onthegomap.planetiler.archive;

import com.onthegomap.planetiler.config.PlanetilerConfig;
import com.onthegomap.planetiler.files.ReadableFilesArchive;
import com.onthegomap.planetiler.files.WriteableFilesArchive;
import com.onthegomap.planetiler.mbtiles.Mbtiles;
import com.onthegomap.planetiler.pmtiles.ReadablePmtiles;
import com.onthegomap.planetiler.pmtiles.WriteablePmtiles;
Expand Down Expand Up @@ -56,6 +58,7 @@ public static WriteableTileArchive newWriter(TileArchiveConfig archive, Planetil
new StreamArchiveConfig(config, options));
case JSON -> WriteableJsonStreamArchive.newWriteToFile(archive.getLocalPath(),
new StreamArchiveConfig(config, options));
case FILES -> WriteableFilesArchive.newWriter(archive.getLocalPath());
};
}

Expand All @@ -73,6 +76,7 @@ public static ReadableTileArchive newReader(TileArchiveConfig archive, Planetile
case CSV, TSV -> throw new UnsupportedOperationException("reading CSV is not supported");
case PROTO, PBF -> throw new UnsupportedOperationException("reading PROTO is not supported");
case JSON -> throw new UnsupportedOperationException("reading JSON is not supported");
case FILES -> ReadableFilesArchive.newReader(archive.getLocalPath());
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.onthegomap.planetiler.files;

import com.onthegomap.planetiler.geo.TileCoord;
import java.nio.file.Path;
import java.nio.file.Paths;

final class FilesArchiveUtils {

static final String PBF_FILE_ENDING = ".pbf";

private FilesArchiveUtils() {}

static Path relativePathFromTileCoord(TileCoord tc) {
return Paths.get(Integer.toString(tc.z()), Integer.toString(tc.x()),
tc.y() + PBF_FILE_ENDING);
}

static Path absolutePathFromTileCoord(Path basePath, TileCoord tc) {
return basePath.resolve(relativePathFromTileCoord(tc));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.onthegomap.planetiler.files;

import static com.onthegomap.planetiler.files.FilesArchiveUtils.PBF_FILE_ENDING;
import static com.onthegomap.planetiler.files.FilesArchiveUtils.absolutePathFromTileCoord;

import com.google.common.base.Preconditions;
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 java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.stream.Stream;
import org.apache.commons.lang3.math.NumberUtils;

public class ReadableFilesArchive implements ReadableTileArchive {

private final Path basePath;

private ReadableFilesArchive(Path basePath) {
this.basePath = basePath;
Preconditions.checkArgument(
Files.isDirectory(basePath),
"require \"" + basePath + "\" to be an existing directory"
);
}

public static ReadableFilesArchive newReader(Path basePath) {
return new ReadableFilesArchive(basePath);
}

@Override
@SuppressWarnings("java:S1168") // returning null is in sync with other mbtiles/pmtiles implementation
public byte[] getTile(int x, int y, int z) {
final Path absolute = absolutePathFromTileCoord(basePath, TileCoord.ofXYZ(x, y, z));
if (!Files.exists(absolute)) {
return null;
}
try {
return Files.readAllBytes(absolute);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

@Override
public CloseableIterator<TileCoord> getAllTileCoords() {

try {
final Stream<TileCoord> it = Files.find(basePath, 3, (p, a) -> a.isRegularFile())
.map(this::mapFileToTileCoord)
.flatMap(Optional::stream);
return CloseableIterator.of(it);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

@Override
public TileArchiveMetadata metadata() {
return null;
}

@Override
public void close() {
// nothing to do here
}

private Optional<TileCoord> mapFileToTileCoord(Path path) {
final Path relative = basePath.relativize(path);
if (relative.getNameCount() != 3) {
return Optional.empty();
}
final int z = NumberUtils.toInt(relative.getName(0).toString(), -1);
if (z < 0) {
return Optional.empty();
}
final int x = NumberUtils.toInt(relative.getName(1).toString(), -1);
if (x < 0) {
return Optional.empty();
}
final String yPbf = relative.getName(2).toString();
int dotIdx = yPbf.indexOf('.');
if (dotIdx < 1) {
return Optional.empty();
}
final int y = NumberUtils.toInt(yPbf.substring(0, dotIdx), -1);
if (y < 0) {
return Optional.empty();
}
if (!PBF_FILE_ENDING.equals(yPbf.substring(dotIdx))) {
return Optional.empty();
}
return Optional.of(TileCoord.ofXYZ(x, y, z));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.onthegomap.planetiler.files;

import com.google.common.base.Preconditions;
import com.onthegomap.planetiler.archive.TileEncodingResult;
import com.onthegomap.planetiler.archive.WriteableTileArchive;
import com.onthegomap.planetiler.geo.TileOrder;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class WriteableFilesArchive implements WriteableTileArchive {

private final Path basePath;

private WriteableFilesArchive(Path basePath) {
this.basePath = basePath;
if (!Files.exists(basePath)) {
mkdirs(basePath);
}
Preconditions.checkArgument(
Files.isDirectory(basePath),
"require \"" + basePath + "\" to be a directory"
);
}

public static WriteableFilesArchive newWriter(Path basePath) {
return new WriteableFilesArchive(basePath);
}

@Override
public boolean deduplicates() {
return false;
}

@Override
public TileOrder tileOrder() {
return TileOrder.TMS;
}

@Override
public TileWriter newTileWriter() {
return new FilesWriter(basePath);
}

@Override
public void close() throws IOException {
// nothing to do here
}

private static void mkdirs(Path p) {
try {
Files.createDirectories(p);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

private static class FilesWriter implements TileWriter {

private final Path basePath;
private Path lastCheckedFolder;

FilesWriter(Path basePath) {
this.basePath = basePath;
this.lastCheckedFolder = basePath;
}

@Override
public void write(TileEncodingResult encodingResult) {

final Path file = FilesArchiveUtils.absolutePathFromTileCoord(basePath, encodingResult.coord());
final Path folder = file.getParent();

// tiny optimization in order to avoid too many unnecessary "folder-exists-checks" (I/O)
if (!lastCheckedFolder.equals(folder) && !Files.exists(folder)) {
mkdirs(folder);
}
lastCheckedFolder = folder;
try {
Files.write(file, encodingResult.tileData());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

@Override
public void close() {
// nothing to do here
}
}
}
Loading

0 comments on commit 6e8effd

Please sign in to comment.