diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/archive/TileArchiveConfig.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/archive/TileArchiveConfig.java
index 0e9f8f39..1abdf00b 100644
--- a/planetiler-core/src/main/java/com/onthegomap/planetiler/archive/TileArchiveConfig.java
+++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/archive/TileArchiveConfig.java
@@ -267,11 +267,11 @@ public record TileArchiveConfig(
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 */,
- false, TileOrder.TMS),
- PMTILES("pmtiles", false, false, TileOrder.HILBERT),
+ false, false, TileOrder.TMS),
+ PMTILES("pmtiles", false, false, false, TileOrder.HILBERT),
// should be before PBF in order to avoid collisions
- FILES("files", true, true, TileOrder.TMS) {
+ FILES("files", true, true, true, TileOrder.TMS) {
@Override
boolean isUriSupported(URI uri) {
final String path = uri.getPath();
@@ -280,25 +280,28 @@ public record TileArchiveConfig(
}
},
- CSV("csv", true, true, TileOrder.TMS),
+ CSV("csv", true, true, false, TileOrder.TMS),
/** identical to {@link Format#CSV} - except for the column separator */
- TSV("tsv", true, true, TileOrder.TMS),
+ TSV("tsv", true, true, false, TileOrder.TMS),
- PROTO("proto", true, true, TileOrder.TMS),
+ PROTO("proto", true, true, false, TileOrder.TMS),
/** identical to {@link Format#PROTO} */
- PBF("pbf", true, true, TileOrder.TMS),
+ PBF("pbf", true, true, false, TileOrder.TMS),
- JSON("json", true, true, TileOrder.TMS);
+ JSON("json", true, true, false, TileOrder.TMS);
private final String id;
private final boolean supportsAppend;
private final boolean supportsConcurrentWrites;
+ private final boolean supportsConcurrentReads;
private final TileOrder order;
- Format(String id, boolean supportsAppend, boolean supportsConcurrentWrites, TileOrder order) {
+ Format(String id, boolean supportsAppend, boolean supportsConcurrentWrites, boolean supportsConcurrentReads,
+ TileOrder order) {
this.id = id;
this.supportsAppend = supportsAppend;
this.supportsConcurrentWrites = supportsConcurrentWrites;
+ this.supportsConcurrentReads = supportsConcurrentReads;
this.order = order;
}
@@ -318,6 +321,10 @@ public record TileArchiveConfig(
return supportsConcurrentWrites;
}
+ public boolean supportsConcurrentReads() {
+ return supportsConcurrentReads;
+ }
+
boolean isUriSupported(URI uri) {
final String path = uri.getPath();
return path != null && path.endsWith("." + id);
diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/archive/TileCopy.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/archive/TileCopy.java
deleted file mode 100644
index 03d8a139..00000000
--- a/planetiler-core/src/main/java/com/onthegomap/planetiler/archive/TileCopy.java
+++ /dev/null
@@ -1,236 +0,0 @@
-package com.onthegomap.planetiler.archive;
-
-import com.onthegomap.planetiler.config.Arguments;
-import com.onthegomap.planetiler.config.CommonConfigs;
-import com.onthegomap.planetiler.stats.Counter;
-import com.onthegomap.planetiler.stats.ProcessInfo;
-import com.onthegomap.planetiler.stats.ProgressLoggers;
-import com.onthegomap.planetiler.stats.Stats;
-import com.onthegomap.planetiler.util.Gzip;
-import com.onthegomap.planetiler.util.Hashing;
-import com.onthegomap.planetiler.worker.WorkerPipeline;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.nio.file.Path;
-import java.time.Duration;
-import java.util.List;
-import java.util.Map;
-import java.util.OptionalLong;
-import java.util.function.Function;
-import java.util.function.UnaryOperator;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Utility to copy/convert tiles and metadata from one archive into another.
- *
- * Example usages:
- *
- *
- * --input=tiles.mbtiles --output=tiles.mbtiles
- * --input=tiles.mbtiles --output=tiles.pmtiles --skip_empty=false
- * --input=tiles.pmtiles --output=tiles.mbtiles
- * --input=tiles.mbtiles --output=tiles/
- * --input=tiles.mbtiles --output=tiles.json --out_tile_compression=gzip
- * --input=tiles.mbtiles --output=tiles.csv --out_tile_compression=none
- * --input=tiles.mbtiles --output=tiles.proto
- *
- */
-public class TileCopy {
-
- private static final Logger LOGGER = LoggerFactory.getLogger(TileCopy.class);
-
- private final TileCopyConfig config;
-
- private final Counter.MultiThreadCounter tilesWrittenOverall = Counter.newMultiThreadCounter();
-
-
- TileCopy(TileCopyConfig config) {
- this.config = config;
- }
-
- public void run() throws IOException {
-
- if (!config.inArchive().exists()) {
- throw new IllegalArgumentException("the input archive does not exist");
- }
-
- config.outArchive().setup(config.force(), config.append(), config.tileWriterThreads());
-
- final var loggers = ProgressLoggers.create()
- .addRateCounter("tiles", tilesWrittenOverall::get);
-
- try (
- var reader = TileArchives.newReader(config.inArchive(), config.inArguments());
- var writer = TileArchives.newWriter(config.outArchive(), config.outArguments())
- ) {
-
- final TileArchiveMetadata inMetadata = getInMetadata(reader);
- final TileArchiveMetadata outMetadata = getOutMetadata(inMetadata);
-
- writer.initialize();
- try (
- var rawTiles = reader.getAllTiles();
- var it = config.skipEmpty() ? rawTiles.filter(t -> t.bytes() != null && t.bytes().length > 0) : rawTiles
- ) {
-
- final var tileConverter = tileConverter(inMetadata.tileCompression(), outMetadata.tileCompression(), writer);
- var pipeline = WorkerPipeline.start("archive", config.stats())
- .readFrom("tiles", () -> it)
- .addBuffer("buffer", config.queueSize())
- .sinkTo("write", config.tileWriterThreads(), itt -> tileWriter(writer, itt, tileConverter));
-
- final var f = pipeline.done().thenRun(() -> writer.finish(outMetadata));
-
- loggers.awaitAndLog(f, config.logInterval());
- }
- }
- }
-
- private void tileWriter(WriteableTileArchive archive, Iterable itt,
- Function tileConverter) {
-
- final Counter tilesWritten = tilesWrittenOverall.counterForThread();
- try (var tileWriter = archive.newTileWriter()) {
- for (Tile t : itt) {
- tileWriter.write(tileConverter.apply(t));
- tilesWritten.inc();
- }
- }
- }
-
- private static Function tileConverter(TileCompression inCompression,
- TileCompression outCompression, WriteableTileArchive writer) {
-
- final UnaryOperator bytesReEncoder = bytesReEncoder(inCompression, outCompression);
- final Function hasher =
- writer.deduplicates() ? b -> OptionalLong.of(Hashing.fnv1a64(b)) :
- b -> OptionalLong.empty();
-
- return t -> new TileEncodingResult(t.coord(), bytesReEncoder.apply(t.bytes()), hasher.apply(t.bytes()));
- }
-
- private static UnaryOperator bytesReEncoder(TileCompression inCompression, TileCompression outCompression) {
- if (inCompression == outCompression) {
- return UnaryOperator.identity();
- } else if (inCompression == TileCompression.GZIP && outCompression == TileCompression.NONE) {
- return Gzip::gunzip;
- } else if (inCompression == TileCompression.NONE && outCompression == TileCompression.GZIP) {
- return Gzip::gzip;
- } else if (inCompression == TileCompression.UNKNWON && outCompression == TileCompression.GZIP) {
- return b -> Gzip.isZipped(b) ? b : Gzip.gzip(b);
- } else if (inCompression == TileCompression.UNKNWON && outCompression == TileCompression.NONE) {
- return b -> Gzip.isZipped(b) ? Gzip.gunzip(b) : b;
- } else {
- throw new IllegalArgumentException("unhandled case: in=" + inCompression + " out=" + outCompression);
- }
- }
-
- private TileArchiveMetadata getInMetadata(ReadableTileArchive reader) {
- TileArchiveMetadata inMetadata = config.inMetadata();
- if (inMetadata == null) {
- inMetadata = reader.metadata();
- if (inMetadata == null) {
- LOGGER.atWarn()
- .log("the input archive does not contain any metadata using fallback - consider passing one via in_metadata");
- inMetadata = fallbackMetadata();
- }
- }
- if (inMetadata.tileCompression() == null) {
- inMetadata = inMetadata.withTileCompression(config.inCompression());
- }
-
- return inMetadata;
- }
-
- private TileArchiveMetadata getOutMetadata(TileArchiveMetadata inMetadata) {
- if (config.outCompression() == TileCompression.UNKNWON && inMetadata.tileCompression() == TileCompression.UNKNWON) {
- return inMetadata.withTileCompression(TileCompression.GZIP);
- } else if (config.outCompression() != TileCompression.UNKNWON) {
- return inMetadata.withTileCompression(config.outCompression());
- } else {
- return inMetadata;
- }
- }
-
- private static TileArchiveMetadata fallbackMetadata() {
- return new TileArchiveMetadata(
- "unknown",
- null,
- null,
- null,
- null,
- TileArchiveMetadata.MVT_FORMAT, // have to guess here that it's pbf
- null,
- null,
- null,
- null,
- new TileArchiveMetadata.TileArchiveMetadataJson(List.of()), // cannot provide any vector layers
- Map.of(),
- null
- );
- }
-
- record TileCopyConfig(
- TileArchiveConfig inArchive,
- TileArchiveConfig outArchive,
- Arguments inArguments,
- Arguments outArguments,
- TileCompression inCompression,
- TileCompression outCompression,
- int tileWriterThreads,
- Duration logInterval,
- Stats stats,
- int queueSize,
- boolean append,
- boolean force,
- TileArchiveMetadata inMetadata,
- boolean skipEmpty
- ) {
- static TileCopyConfig fromArguments(Arguments baseArguments) {
-
- final Arguments inArguments = baseArguments.withPrefix("in");
- final Arguments outArguments = baseArguments.withPrefix("out");
- final Arguments baseOrOutArguments = outArguments.orElse(baseArguments);
-
- final Path inMetadataPath = inArguments.file("metadata", "path to metadata.json to use instead", null);
- final TileArchiveMetadata inMetadata;
- if (inMetadataPath != null) {
- try {
- inMetadata =
- TileArchiveMetadataDeSer.mbtilesMapper().readValue(inMetadataPath.toFile(), TileArchiveMetadata.class);
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- } else {
- inMetadata = null;
- }
-
- return new TileCopyConfig(
- TileArchiveConfig.from(baseArguments.getString("input", "input tile archive")),
- TileArchiveConfig.from(baseArguments.getString("output", "output tile archive")),
- inArguments,
- outArguments,
- getTileCompressionArg(inArguments, "the input tile compression"),
- getTileCompressionArg(outArguments, "the output tile compression"),
- CommonConfigs.tileWriterThreads(baseOrOutArguments),
- CommonConfigs.logInterval(baseArguments),
- baseArguments.getStats(),
- Math.max(100, (int) (5_000d * ProcessInfo.getMaxMemoryBytes() / 100_000_000_000d)),
- CommonConfigs.appendToArchive(baseOrOutArguments),
- CommonConfigs.force(baseOrOutArguments),
- inMetadata,
- baseArguments.getBoolean("skip_empty", "skip empty (null/zero-bytes) tiles", false)
- );
- }
- }
-
- private static TileCompression getTileCompressionArg(Arguments args, String description) {
- return args.getObject("tile_compression", description, TileCompression.UNKNWON,
- v -> TileCompression.findById(v).orElseThrow());
- }
-
- public static void main(String[] args) throws IOException {
- new TileCopy(TileCopyConfig.fromArguments(Arguments.fromEnvOrArgs(args))).run();
- }
-}
diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/config/Arguments.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/config/Arguments.java
index 879e20e3..70281ff6 100644
--- a/planetiler-core/src/main/java/com/onthegomap/planetiler/config/Arguments.java
+++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/config/Arguments.java
@@ -45,11 +45,17 @@ public class Arguments {
private final UnaryOperator provider;
private final Supplier extends Collection> keys;
+ private final String logKeyPrefix;
private boolean silent = false;
- private Arguments(UnaryOperator provider, Supplier extends Collection> keys) {
+ private Arguments(UnaryOperator provider, Supplier extends Collection> keys, String logKeyPrefix) {
this.provider = provider;
this.keys = keys;
+ this.logKeyPrefix = logKeyPrefix;
+ }
+
+ private Arguments(UnaryOperator provider, Supplier extends Collection> keys) {
+ this(provider, keys, "");
}
/**
@@ -65,7 +71,7 @@ public class Arguments {
}
static Arguments fromJvmProperties(UnaryOperator getter, Supplier extends Collection> keys) {
- return fromPrefixed(getter, keys, "planetiler", ".", false);
+ return fromPrefixed(getter, keys, "planetiler", ".", false, "");
}
/**
@@ -81,7 +87,7 @@ public class Arguments {
}
static Arguments fromEnvironment(UnaryOperator getter, Supplier> keys) {
- return fromPrefixed(getter, keys, "PLANETILER", "_", true);
+ return fromPrefixed(getter, keys, "PLANETILER", "_", true, "");
}
/**
@@ -213,21 +219,22 @@ public class Arguments {
}
private static Arguments from(UnaryOperator provider, Supplier extends Collection> rawKeys,
- UnaryOperator forward, UnaryOperator reverse) {
+ UnaryOperator forward, UnaryOperator reverse, String logKeyPrefix) {
Supplier> keys = () -> rawKeys.get().stream().flatMap(key -> {
String reversed = reverse.apply(key);
return normalize(key).equals(normalize(reversed)) ? Stream.empty() : Stream.of(reversed);
}).toList();
- return new Arguments(key -> provider.apply(forward.apply(key)), keys);
+ return new Arguments(key -> provider.apply(forward.apply(key)), keys, logKeyPrefix);
}
private static Arguments fromPrefixed(UnaryOperator provider, Supplier extends Collection> keys,
- String prefix, String separator, boolean uppperCase) {
+ String prefix, String separator, boolean uppperCase, String logKeyPrefix) {
var prefixRegex = Pattern.compile("^" + Pattern.quote(normalize(prefix + separator, separator, uppperCase)),
Pattern.CASE_INSENSITIVE);
return from(provider, keys,
key -> normalize(prefix + separator + key, separator, uppperCase),
- key -> normalize(prefixRegex.matcher(key).replaceFirst(""))
+ key -> normalize(prefixRegex.matcher(key).replaceFirst("")),
+ logKeyPrefix
);
}
@@ -262,7 +269,8 @@ public class Arguments {
() -> Stream.concat(
other.keys.get().stream(),
keys.get().stream()
- ).distinct().toList()
+ ).distinct().toList(),
+ other.logKeyPrefix
);
if (silent) {
result.silence();
@@ -307,7 +315,7 @@ public class Arguments {
protected void logArgValue(String key, String description, Object result) {
if (!silent && LOGGER.isDebugEnabled()) {
- LOGGER.debug("argument: {}={} ({})", key.replaceFirst("\\|.*$", ""), result, description);
+ LOGGER.debug("argument: {}{}={} ({})", logKeyPrefix, key.replaceFirst("\\|.*$", ""), result, description);
}
}
@@ -532,7 +540,7 @@ public class Arguments {
* Returns a new arguments instance that translates requests for a {@code "key"} to {@code "prefix_key"}.
*/
public Arguments withPrefix(String prefix) {
- return fromPrefixed(provider, keys, prefix, "_", false);
+ return fromPrefixed(provider, keys, prefix, "_", false, logKeyPrefix + prefix + "_");
}
/** Returns a view of this instance, that only supports requests for {@code allowedKeys}. */
diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/copy/TileCopy.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/copy/TileCopy.java
new file mode 100644
index 00000000..3155a5ee
--- /dev/null
+++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/copy/TileCopy.java
@@ -0,0 +1,131 @@
+package com.onthegomap.planetiler.copy;
+
+import static com.onthegomap.planetiler.worker.Worker.joinFutures;
+
+import com.onthegomap.planetiler.archive.TileArchives;
+import com.onthegomap.planetiler.config.Arguments;
+import com.onthegomap.planetiler.stats.Counter;
+import com.onthegomap.planetiler.stats.ProgressLoggers;
+import com.onthegomap.planetiler.worker.WorkQueue;
+import com.onthegomap.planetiler.worker.WorkerPipeline;
+import java.io.IOException;
+import java.time.Duration;
+
+/**
+ * Utility to copy/convert tiles and metadata from one archive into another.
+ *
+ * Example usages:
+ *
+ *
+ * --input=tiles.mbtiles --output=tiles.mbtiles
+ * --input=tiles.mbtiles --output=tiles.pmtiles --skip_empty=false
+ * --input=tiles.pmtiles --output=tiles.mbtiles
+ * --input=tiles.mbtiles --output=tiles/
+ * --input=tiles.mbtiles --output=tiles.json --out_tile_compression=gzip
+ * --input=tiles.mbtiles --output=tiles.csv --out_tile_compression=none
+ * --input=tiles.mbtiles --output=tiles.proto
+ *
+ */
+public class TileCopy {
+
+ private final TileCopyConfig config;
+
+ private final Counter.MultiThreadCounter tilesReadOverall = Counter.newMultiThreadCounter();
+ private final Counter.MultiThreadCounter tilesProcessedOverall = Counter.newMultiThreadCounter();
+ private final Counter.MultiThreadCounter tilesWrittenOverall = Counter.newMultiThreadCounter();
+
+
+ TileCopy(TileCopyConfig config) {
+ this.config = config;
+ }
+
+ public void run() throws IOException {
+
+ if (!config.inArchive().exists()) {
+ throw new IllegalArgumentException("the input archive does not exist");
+ }
+
+ config.outArchive().setup(config.force(), config.append(), config.tileWriterThreads());
+
+ final var loggers = ProgressLoggers.create()
+ .addRateCounter("tiles_read", tilesReadOverall::get)
+ .addRateCounter("tiles_processed", tilesProcessedOverall::get)
+ .addRateCounter("tiles_written", tilesWrittenOverall::get);
+
+ try (
+ var reader = TileArchives.newReader(config.inArchive(), config.inArguments());
+ var writer = TileArchives.newWriter(config.outArchive(), config.outArguments())
+ ) {
+
+ final TileCopyContext context = TileCopyContext.create(reader, writer, config);
+
+ final var pipeline = WorkerPipeline.start("copy", config.stats());
+ final WorkQueue resultQueue =
+ new WorkQueue<>("results", config.workQueueCapacity(), config.workQueueMaxBatch(), config.stats());
+
+ try (
+ var it = TileCopyWorkItemGenerators.create(context)
+ ) {
+
+ writer.initialize();
+
+ final var readerBranch = pipeline
+ .fromGenerator("iterator", next -> {
+ try (resultQueue) {
+ while (it.hasNext()) {
+ final TileCopyWorkItem t = it.next();
+ resultQueue.accept(t); // put to queue immediately => retain order
+ next.accept(t);
+ }
+ }
+ })
+ .addBuffer("to_read", config.workQueueCapacity(), config.workQueueMaxBatch())
+ .addWorker("read", config.tileReaderThreads(), (prev, next) -> {
+ final Counter tilesRead = tilesReadOverall.counterForThread();
+ for (var item : prev) {
+ item.loadOriginalTileData();
+ tilesRead.inc();
+ next.accept(item);
+ }
+ })
+ .addBuffer("to_process", config.workQueueCapacity(), config.workQueueMaxBatch())
+ .sinkTo("process", config.tileReaderThreads(), prev -> {
+ final Counter tilesProcessed = tilesProcessedOverall.counterForThread();
+ for (var item : prev) {
+ item.process();
+ tilesProcessed.inc();
+ }
+ });
+
+ final var writerBranch = pipeline.readFromQueue(resultQueue)
+ .sinkTo("write", config.tileWriterThreads(), prev -> {
+ final Counter tilesWritten = tilesWrittenOverall.counterForThread();
+ try (var tileWriter = writer.newTileWriter()) {
+ for (var item : prev) {
+ var result = item.toTileEncodingResult();
+ if (result.tileData() == null && config.skipEmpty()) {
+ continue;
+ }
+ tileWriter.write(result);
+ tilesWritten.inc();
+ }
+ }
+ });
+
+ final var writerDone = writerBranch.done().thenRun(() -> writer.finish(context.outMetadata()));
+ final var readerDone = readerBranch.done();
+
+ loggers
+ .newLine()
+ .addPipelineStats(readerBranch)
+ .addPipelineStats(writerBranch);
+
+ loggers.awaitAndLog(joinFutures(writerDone, readerDone), Duration.ofSeconds(1));
+ }
+ }
+ }
+
+ public static void main(String[] args) throws IOException {
+ new TileCopy(TileCopyConfig.fromArguments(Arguments.fromEnvOrArgs(args))).run();
+ }
+}
diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/copy/TileCopyConfig.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/copy/TileCopyConfig.java
new file mode 100644
index 00000000..c54ca229
--- /dev/null
+++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/copy/TileCopyConfig.java
@@ -0,0 +1,108 @@
+package com.onthegomap.planetiler.copy;
+
+import com.onthegomap.planetiler.archive.TileArchiveConfig;
+import com.onthegomap.planetiler.archive.TileArchiveMetadata;
+import com.onthegomap.planetiler.archive.TileArchiveMetadataDeSer;
+import com.onthegomap.planetiler.archive.TileCompression;
+import com.onthegomap.planetiler.config.Arguments;
+import com.onthegomap.planetiler.config.CommonConfigs;
+import com.onthegomap.planetiler.geo.TileOrder;
+import com.onthegomap.planetiler.stats.ProcessInfo;
+import com.onthegomap.planetiler.stats.Stats;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Path;
+import java.time.Duration;
+import org.locationtech.jts.geom.Envelope;
+
+record TileCopyConfig(
+ TileArchiveConfig inArchive,
+ TileArchiveConfig outArchive,
+ Arguments inArguments,
+ Arguments outArguments,
+ TileCompression inCompression,
+ TileCompression outCompression,
+ int tileWriterThreads,
+ int tileReaderThreads,
+ int tileProcessorThreads,
+ Duration logInterval,
+ Stats stats,
+ int queueSize,
+ boolean append,
+ boolean force,
+ TileArchiveMetadata inMetadata,
+ boolean skipEmpty,
+ Envelope filterBounds,
+ int filterMinzoom,
+ int filterMaxzoom,
+ boolean scanTilesInOrder,
+ TileOrder outputTileOrder,
+ int workQueueCapacity,
+ int workQueueMaxBatch
+) {
+
+ TileCopyConfig {
+ if (tileReaderThreads > 1 && !inArchive.format().supportsConcurrentReads()) {
+ throw new IllegalArgumentException(inArchive.format().id() + " does not support concurrent reads");
+ }
+ if (tileWriterThreads > 1 && !outArchive.format().supportsConcurrentReads()) {
+ throw new IllegalArgumentException(outArchive.format().id() + " does not support concurrent writes");
+ }
+ if (filterMinzoom > filterMaxzoom) {
+ throw new IllegalArgumentException("require minzoom <= maxzoom");
+ }
+ }
+
+ static TileCopyConfig fromArguments(Arguments baseArguments) {
+
+ final Arguments inArguments = baseArguments.withPrefix("in");
+ final Arguments outArguments = baseArguments.withPrefix("out");
+ final Arguments baseOrOutArguments = outArguments.orElse(baseArguments);
+
+ final Path inMetadataPath = inArguments.file("metadata", "path to metadata.json to use instead", null);
+ final TileArchiveMetadata inMetadata;
+ if (inMetadataPath != null) {
+ try {
+ inMetadata =
+ TileArchiveMetadataDeSer.mbtilesMapper().readValue(inMetadataPath.toFile(), TileArchiveMetadata.class);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ } else {
+ inMetadata = null;
+ }
+
+ return new TileCopyConfig(
+ TileArchiveConfig.from(baseArguments.getString("input", "input tile archive")),
+ TileArchiveConfig.from(baseArguments.getString("output", "output tile archive")),
+ inArguments,
+ outArguments,
+ getTileCompressionArg(inArguments, "the input tile compression"),
+ getTileCompressionArg(outArguments, "the output tile compression"),
+ CommonConfigs.tileWriterThreads(baseOrOutArguments),
+ baseArguments.getInteger("tile_read_threads", "number of threads used to read tile data", 1),
+ baseArguments.getInteger("tile_process_threads", "number of threads used to process tile data",
+ Math.max(1, Runtime.getRuntime().availableProcessors() - 2)),
+ CommonConfigs.logInterval(baseArguments),
+ baseArguments.getStats(),
+ Math.max(100, (int) (5_000d * ProcessInfo.getMaxMemoryBytes() / 100_000_000_000d)),
+ CommonConfigs.appendToArchive(baseOrOutArguments),
+ CommonConfigs.force(baseOrOutArguments),
+ inMetadata,
+ baseArguments.getBoolean("skip_empty", "skip empty (null/zero-bytes) tiles", true),
+ baseArguments.bounds("filter_bounds", "the bounds to filter"),
+ baseArguments.getInteger("filter_minzoom", "the min zoom", 0),
+ baseArguments.getInteger("filter_maxzoom", "the max zoom", 14),
+ baseArguments.getBoolean("scan_tiles_in_order", "output the tiles in the same order they are in the", true),
+ baseArguments.getObject("output_tile_order", "the output tile order (if not scanned)", null,
+ s -> s == null ? null : TileOrder.valueOf(s.toUpperCase())),
+ 100_000,
+ 100
+ );
+ }
+
+ private static TileCompression getTileCompressionArg(Arguments args, String description) {
+ return args.getObject("tile_compression", description, TileCompression.UNKNOWN,
+ v -> TileCompression.findById(v).orElseThrow());
+ }
+}
diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/copy/TileCopyContext.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/copy/TileCopyContext.java
new file mode 100644
index 00000000..da13dd81
--- /dev/null
+++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/copy/TileCopyContext.java
@@ -0,0 +1,123 @@
+package com.onthegomap.planetiler.copy;
+
+import com.onthegomap.planetiler.archive.ReadableTileArchive;
+import com.onthegomap.planetiler.archive.TileArchiveMetadata;
+import com.onthegomap.planetiler.archive.TileCompression;
+import com.onthegomap.planetiler.archive.WriteableTileArchive;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Stream;
+import org.locationtech.jts.geom.Envelope;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+record TileCopyContext(
+ TileArchiveMetadata inMetadata,
+ TileArchiveMetadata outMetadata,
+ TileCopyConfig config,
+ ReadableTileArchive reader,
+ WriteableTileArchive writer
+) {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TileCopyContext.class);
+
+ TileCompression inputCompression() {
+ return inMetadata.tileCompression();
+ }
+
+ TileCompression outputCompression() {
+ return outMetadata.tileCompression();
+ }
+
+ static TileCopyContext create(ReadableTileArchive reader, WriteableTileArchive writer, TileCopyConfig config) {
+ final TileArchiveMetadata inMetadata = getInMetadata(reader, config);
+ final TileArchiveMetadata outMetadata = getOutMetadata(inMetadata, config);
+ return new TileCopyContext(
+ inMetadata,
+ outMetadata,
+ config,
+ reader,
+ writer
+ );
+ }
+
+ private static TileArchiveMetadata getInMetadata(ReadableTileArchive inArchive, TileCopyConfig config) {
+ TileArchiveMetadata inMetadata = config.inMetadata();
+ if (inMetadata == null) {
+ inMetadata = inArchive.metadata();
+ if (inMetadata == null) {
+ LOGGER.atWarn()
+ .log("the input archive does not contain any metadata using fallback - consider passing one via in_metadata");
+ inMetadata = fallbackMetadata();
+ }
+ }
+ if (inMetadata.tileCompression() == null) {
+ inMetadata = inMetadata.withTileCompression(config.inCompression());
+ }
+
+ return inMetadata;
+ }
+
+ private static TileArchiveMetadata getOutMetadata(TileArchiveMetadata inMetadata, TileCopyConfig config) {
+
+ final TileCompression tileCompression;
+ if (config.outCompression() == TileCompression.UNKNOWN && inMetadata.tileCompression() == TileCompression.UNKNOWN) {
+ tileCompression = TileCompression.GZIP;
+ } else if (config.outCompression() != TileCompression.UNKNOWN) {
+ tileCompression = config.outCompression();
+ } else {
+ tileCompression = inMetadata.tileCompression();
+ }
+
+ final Envelope bounds;
+ if (config.filterBounds() != null) {
+ bounds = config.filterBounds();
+ } else if (inMetadata.bounds() != null) {
+ bounds = inMetadata.bounds();
+ } else {
+ bounds = null;
+ }
+
+ final int minzoom = Stream.of(inMetadata.minzoom(), config.filterMinzoom()).filter(Objects::nonNull)
+ .mapToInt(Integer::intValue).max().orElse(0);
+
+ final int maxzoom = Stream.of(inMetadata.maxzoom(), config.filterMaxzoom()).filter(Objects::nonNull)
+ .mapToInt(Integer::intValue).min().orElse(0);
+
+ return new TileArchiveMetadata(
+ inMetadata.name(),
+ inMetadata.description(),
+ inMetadata.attribution(),
+ inMetadata.version(),
+ inMetadata.type(),
+ inMetadata.format(),
+ bounds,
+ inMetadata.center(),
+ minzoom,
+ maxzoom,
+ inMetadata.json(),
+ inMetadata.others(),
+ tileCompression
+ );
+ }
+
+ private static TileArchiveMetadata fallbackMetadata() {
+ return new TileArchiveMetadata(
+ "unknown",
+ null,
+ null,
+ null,
+ null,
+ TileArchiveMetadata.MVT_FORMAT, // have to guess here that it's pbf
+ null,
+ null,
+ 0,
+ 14,
+ new TileArchiveMetadata.TileArchiveMetadataJson(List.of()), // cannot provide any vector layers
+ Map.of(),
+ null
+ );
+ }
+
+}
diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/copy/TileCopyWorkItem.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/copy/TileCopyWorkItem.java
new file mode 100644
index 00000000..505ea557
--- /dev/null
+++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/copy/TileCopyWorkItem.java
@@ -0,0 +1,51 @@
+package com.onthegomap.planetiler.copy;
+
+import com.onthegomap.planetiler.archive.TileEncodingResult;
+import com.onthegomap.planetiler.geo.TileCoord;
+import java.util.OptionalLong;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.function.UnaryOperator;
+
+public class TileCopyWorkItem {
+ private final TileCoord coord;
+ private final Supplier originalTileDataLoader;
+ private final UnaryOperator reEncoder;
+ private final Function hasher;
+ private final CompletableFuture originalData = new CompletableFuture<>();
+ private final CompletableFuture reEncodedData = new CompletableFuture<>();
+ private final CompletableFuture reEncodedDataHash = new CompletableFuture<>();
+
+ TileCopyWorkItem(
+ TileCoord coord,
+ Supplier originalTileDataLoader,
+ UnaryOperator reEncoder,
+ Function hasher
+ ) {
+ this.coord = coord;
+ this.originalTileDataLoader = originalTileDataLoader;
+ this.reEncoder = reEncoder;
+ this.hasher = hasher;
+ }
+
+ public TileCoord getCoord() {
+ return coord;
+ }
+
+ void loadOriginalTileData() {
+ originalData.complete(originalTileDataLoader.get());
+ }
+
+ void process() throws ExecutionException, InterruptedException {
+ final var reEncoded = reEncoder.apply(originalData.get());
+ final var hash = hasher.apply(reEncoded);
+ reEncodedData.complete(reEncoded);
+ reEncodedDataHash.complete(hash);
+ }
+
+ TileEncodingResult toTileEncodingResult() throws ExecutionException, InterruptedException {
+ return new TileEncodingResult(coord, reEncodedData.get(), reEncodedDataHash.get());
+ }
+}
diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/copy/TileCopyWorkItemGenerators.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/copy/TileCopyWorkItemGenerators.java
new file mode 100644
index 00000000..db89b4f8
--- /dev/null
+++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/copy/TileCopyWorkItemGenerators.java
@@ -0,0 +1,147 @@
+package com.onthegomap.planetiler.copy;
+
+import com.onthegomap.planetiler.archive.Tile;
+import com.onthegomap.planetiler.archive.TileArchiveConfig;
+import com.onthegomap.planetiler.config.Bounds;
+import com.onthegomap.planetiler.geo.GeoUtils;
+import com.onthegomap.planetiler.geo.TileCoord;
+import com.onthegomap.planetiler.geo.TileExtents;
+import com.onthegomap.planetiler.geo.TileOrder;
+import com.onthegomap.planetiler.util.CloseableIterator;
+import com.onthegomap.planetiler.util.Hashing;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.OptionalLong;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.UnaryOperator;
+import org.locationtech.jts.geom.Envelope;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+final class TileCopyWorkItemGenerators {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TileCopyWorkItemGenerators.class);
+
+ private TileCopyWorkItemGenerators() {}
+
+ @SuppressWarnings("java:S2095")
+ static CloseableIterator create(TileCopyContext context) {
+
+ final TileArchiveConfig.Format inFormat = context.config().inArchive().format();
+ final TileArchiveConfig.Format outFormat = context.config().outArchive().format();
+
+ final boolean inOrder;
+ if (outFormat == TileArchiveConfig.Format.FILES) {
+ inOrder = true; // always use the input formats native order when outputting files
+ } else if (inFormat == TileArchiveConfig.Format.FILES) {
+ inOrder = false; // never use the inOrder looper when using files since there's no order guarantee
+ } else {
+ inOrder = context.config().scanTilesInOrder();
+ }
+
+ final int minzoom = context.outMetadata().minzoom();
+ final int maxzoom = context.outMetadata().maxzoom();
+ final Envelope filterBounds =
+ context.outMetadata().bounds() == null ? Bounds.WORLD.world() :
+ GeoUtils.toWorldBounds(context.outMetadata().bounds());
+ final var boundsFilter = TileExtents.computeFromWorldBounds(maxzoom, filterBounds);
+ final Predicate zoomFilter = i -> i.getCoord().z() >= minzoom && i.getCoord().z() <= maxzoom;
+
+ final ProcessorArgs processorArgs = new ProcessorArgs(
+ TileDataReEncoders.create(context),
+ context.writer().deduplicates() ? b -> OptionalLong.of(Hashing.fnv1a64(b)) :
+ b -> OptionalLong.empty()
+ );
+
+ if (inOrder) {
+ CloseableIterator it = new EagerInOrder(context.reader().getAllTiles(), processorArgs)
+ .filter(zoomFilter);
+ if (!Objects.equals(context.inMetadata().bounds(), context.outMetadata().bounds())) {
+ it = it.filter(i -> boundsFilter.test(i.getCoord()));
+ }
+ return it;
+
+ } else {
+
+ final boolean warnPoorlySupported = switch (inFormat) {
+ case CSV, TSV, PROTO, PBF, JSON -> true;
+ case PMTILES, MBTILES, FILES -> false;
+ };
+ if (warnPoorlySupported) {
+ LOGGER.atWarn().log("{} random access is very slow", inFormat.id());
+ }
+
+ final TileOrder tileOrder = Optional.ofNullable(context.config().outputTileOrder())
+ .orElse(context.writer().tileOrder());
+
+ return new TileOrderLoop(tileOrder, minzoom, maxzoom, context.reader()::getTile, processorArgs)
+ .filter(i -> boundsFilter.test(i.getCoord()));
+ }
+ }
+
+ private record EagerInOrder(
+ CloseableIterator it,
+ ProcessorArgs processorArgs
+ ) implements CloseableIterator {
+
+ @Override
+ public void close() {
+ it.close();
+ }
+
+ @Override
+ public boolean hasNext() {
+ return it.hasNext();
+ }
+
+ @Override
+ public TileCopyWorkItem next() {
+ if (!it.hasNext()) {
+ throw new NoSuchElementException();
+ }
+ final Tile t = it.next();
+ return new TileCopyWorkItem(t.coord(), t::bytes, processorArgs.reEncoder(), processorArgs.hasher());
+ }
+ }
+
+ private static class TileOrderLoop implements CloseableIterator {
+
+ private final TileOrder tileOrder;
+ private final int max;
+ private final Function dataLoader;
+ private final ProcessorArgs processorArgs;
+ private int current;
+
+ TileOrderLoop(TileOrder tileOrder, int minZoom, int maxZoom, Function dataLoader,
+ ProcessorArgs processorArgs) {
+ this.tileOrder = tileOrder;
+ this.current = TileCoord.startIndexForZoom(minZoom);
+ this.max = TileCoord.endIndexForZoom(maxZoom);
+ this.dataLoader = dataLoader;
+ this.processorArgs = processorArgs;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return current <= max;
+ }
+
+ @Override
+ public TileCopyWorkItem next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ final TileCoord c = tileOrder.decode(current++);
+ return new TileCopyWorkItem(c, () -> dataLoader.apply(c), processorArgs.reEncoder(), processorArgs.hasher());
+ }
+
+ @Override
+ public void close() {
+ // nothing to close
+ }
+ }
+
+ private record ProcessorArgs(UnaryOperator reEncoder, Function hasher) {}
+}
diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/copy/TileDataReEncoders.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/copy/TileDataReEncoders.java
new file mode 100644
index 00000000..9ffcd366
--- /dev/null
+++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/copy/TileDataReEncoders.java
@@ -0,0 +1,31 @@
+package com.onthegomap.planetiler.copy;
+
+import com.onthegomap.planetiler.archive.TileCompression;
+import com.onthegomap.planetiler.util.Gzip;
+import java.util.function.UnaryOperator;
+
+final class TileDataReEncoders {
+
+ private TileDataReEncoders() {}
+
+ static UnaryOperator create(TileCopyContext c) {
+ // for now just one - but compose multiple as needed in the future (decompress, do something, compress)
+ return reCompressor(c.inputCompression(), c.outputCompression());
+ }
+
+ private static UnaryOperator reCompressor(TileCompression inCompression, TileCompression outCompression) {
+ if (inCompression == outCompression) {
+ return b -> b;
+ } else if (inCompression == TileCompression.GZIP && outCompression == TileCompression.NONE) {
+ return Gzip::gunzip;
+ } else if (inCompression == TileCompression.NONE && outCompression == TileCompression.GZIP) {
+ return Gzip::gzip;
+ } else if (inCompression == TileCompression.UNKNOWN && outCompression == TileCompression.GZIP) {
+ return b -> Gzip.isZipped(b) ? b : Gzip.gzip(b);
+ } else if (inCompression == TileCompression.UNKNOWN && outCompression == TileCompression.NONE) {
+ return b -> Gzip.isZipped(b) ? Gzip.gunzip(b) : b;
+ } else {
+ throw new IllegalArgumentException("unhandled case: in=" + inCompression + " out=" + outCompression);
+ }
+ }
+}
diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/TileCoord.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/TileCoord.java
index 6d6370a7..8cc45144 100644
--- a/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/TileCoord.java
+++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/geo/TileCoord.java
@@ -42,10 +42,14 @@ public record TileCoord(int encoded, int x, int y, int z) implements Comparable<
}
}
- private static int startIndexForZoom(int z) {
+ public static int startIndexForZoom(int z) {
return ZOOM_START_INDEX[z];
}
+ public static int endIndexForZoom(int z) {
+ return ZOOM_START_INDEX[z] + (1 << z) * (1 << z) - 1;
+ }
+
private static int zoomForIndex(int idx) {
for (int z = MAX_MAXZOOM; z >= 0; z--) {
if (ZOOM_START_INDEX[z] <= idx) {
diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/stream/ReadableProtoStreamArchive.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/stream/ReadableProtoStreamArchive.java
index 98fb9681..ff35e5ba 100644
--- a/planetiler-core/src/main/java/com/onthegomap/planetiler/stream/ReadableProtoStreamArchive.java
+++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/stream/ReadableProtoStreamArchive.java
@@ -125,7 +125,7 @@ public class ReadableProtoStreamArchive extends ReadableStreamArchive TileCompression.UNKNWON;
+ case TILE_COMPRESSION_UNSPECIFIED, UNRECOGNIZED -> TileCompression.UNKNOWN;
case TILE_COMPRESSION_GZIP -> TileCompression.GZIP;
case TILE_COMPRESSION_NONE -> TileCompression.NONE;
};
diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/TestUtils.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/TestUtils.java
index 8d847228..bc30a637 100644
--- a/planetiler-core/src/test/java/com/onthegomap/planetiler/TestUtils.java
+++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/TestUtils.java
@@ -80,7 +80,7 @@ public class TestUtils {
public static final TileArchiveMetadata MAX_METADATA_DESERIALIZED =
new TileArchiveMetadata("name", "description", "attribution", "version", "type", "format", new Envelope(0, 1, 2, 3),
- new Coordinate(1.3, 3.7, 1.0), 2, 3,
+ new Coordinate(1.3, 3.7, 1.0), 0, 8,
TileArchiveMetadata.TileArchiveMetadataJson.create(
List.of(
new LayerAttrStats.VectorLayer("vl0",
@@ -102,8 +102,8 @@ public class TestUtils {
"version":"version",
"type":"type",
"format":"format",
- "minzoom":"2",
- "maxzoom":"3",
+ "minzoom":"0",
+ "maxzoom":"8",
"compression":"gzip",
"bounds":"0,2,1,3",
"center":"1.3,3.7,1",
diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/archive/TileCopyTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/copy/TileCopyTest.java
similarity index 80%
rename from planetiler-core/src/test/java/com/onthegomap/planetiler/archive/TileCopyTest.java
rename to planetiler-core/src/test/java/com/onthegomap/planetiler/copy/TileCopyTest.java
index c3e5b70d..983361a7 100644
--- a/planetiler-core/src/test/java/com/onthegomap/planetiler/archive/TileCopyTest.java
+++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/copy/TileCopyTest.java
@@ -1,4 +1,4 @@
-package com.onthegomap.planetiler.archive;
+package com.onthegomap.planetiler.copy;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -55,7 +55,7 @@ class TileCopyTest {
"output", archiveOutPath.toString()
)).orElse(Arguments.of(arguments));
- new TileCopy(TileCopy.TileCopyConfig.fromArguments(args)).run();
+ new TileCopy(TileCopyConfig.fromArguments(args)).run();
if (archiveDataOut.contains("{")) {
final List expectedLines = Arrays.stream(archiveDataOut.split("\n")).toList();
@@ -142,28 +142,30 @@ class TileCopyTest {
"csv to json - use fallback metadata",
ARCHIVE_0_CSV_COMPRESSION_NONE,
ARCHIVE_0_JSON_BASE.formatted(
- "{\"name\":\"unknown\",\"format\":\"pbf\",\"json\":\"{\\\"vector_layers\\\":[]}\",\"compression\":\"none\"}"),
+ "{\"name\":\"unknown\",\"format\":\"pbf\",\"minzoom\":\"0\",\"maxzoom\":\"14\",\"json\":\"{\\\"vector_layers\\\":[]}\",\"compression\":\"none\"}"),
Map.of("out_tile_compression", "none")
),
argsOf(
"csv to json - use external metadata",
ARCHIVE_0_CSV_COMPRESSION_NONE,
- ARCHIVE_0_JSON_BASE.formatted("{\"name\":\"blub\",\"compression\":\"none\"}"),
+ ARCHIVE_0_JSON_BASE
+ .formatted("{\"name\":\"blub\",\"compression\":\"none\",\"minzoom\":\"0\",\"maxzoom\":\"14\"}"),
Map.of("out_tile_compression", "none", "in_metadata", "blub")
),
argsOf(
"csv to json - null handling",
replaceBase64(ARCHIVE_0_CSV_COMPRESSION_NONE, ""),
- replaceBase64(ARCHIVE_0_JSON_BASE.formatted("{\"name\":\"blub\",\"compression\":\"gzip\"}"), "null")
+ replaceBase64(ARCHIVE_0_JSON_BASE
+ .formatted("{\"name\":\"blub\",\"compression\":\"gzip\",\"minzoom\":\"0\",\"maxzoom\":\"14\"}"), "null")
.replace(",\"encodedData\":\"null\"", ""),
- Map.of("in_metadata", "blub")
+ Map.of("in_metadata", "blub", "skip_empty", "false")
),
argsOf(
"json to csv - null handling",
replaceBase64(ARCHIVE_0_JSON_BASE.formatted("null"), "null")
.replace(",\"encodedData\":\"null\"", ""),
replaceBase64(ARCHIVE_0_CSV_COMPRESSION_NONE, ""),
- Map.of()
+ Map.of("skip_empty", "false")
),
argsOf(
"csv to csv - empty skipping on",
@@ -187,6 +189,52 @@ class TileCopyTest {
1,2,3,AQ==
""",
Map.of("skip_empty", "false", "out_tile_compression", "none")
+ ),
+ argsOf(
+ "tiles in order",
+ """
+ 1,2,3,AQ==
+ 0,0,0,AA==
+ """,
+ """
+ 1,2,3,AQ==
+ 0,0,0,AA==
+ """,
+ Map.of("out_tile_compression", "none")
+ ),
+ argsOf(
+ "tiles re-order",
+ """
+ 0,0,1,AQ==
+ 0,0,0,AA==
+ """,
+ """
+ 0,0,0,AA==
+ 0,0,1,AQ==
+ """,
+ Map.of("out_tile_compression", "none", "scan_tiles_in_order", "false", "filter_maxzoom", "1")
+ ),
+ argsOf(
+ "filter min zoom",
+ """
+ 0,0,0,AA==
+ 0,0,1,AQ==
+ """,
+ """
+ 0,0,1,AQ==
+ """,
+ Map.of("out_tile_compression", "none", "filter_minzoom", "1")
+ ),
+ argsOf(
+ "filter max zoom",
+ """
+ 0,0,1,AQ==
+ 0,0,0,AA==
+ """,
+ """
+ 0,0,0,AA==
+ """,
+ Map.of("out_tile_compression", "none", "filter_maxzoom", "0")
)
);
}
diff --git a/planetiler-dist/src/main/java/com/onthegomap/planetiler/Main.java b/planetiler-dist/src/main/java/com/onthegomap/planetiler/Main.java
index ecdb3300..0387eb1e 100644
--- a/planetiler-dist/src/main/java/com/onthegomap/planetiler/Main.java
+++ b/planetiler-dist/src/main/java/com/onthegomap/planetiler/Main.java
@@ -2,9 +2,9 @@ package com.onthegomap.planetiler;
import static java.util.Map.entry;
-import com.onthegomap.planetiler.archive.TileCopy;
import com.onthegomap.planetiler.benchmarks.LongLongMapBench;
import com.onthegomap.planetiler.benchmarks.OpenMapTilesMapping;
+import com.onthegomap.planetiler.copy.TileCopy;
import com.onthegomap.planetiler.custommap.ConfiguredMapMain;
import com.onthegomap.planetiler.custommap.validator.SchemaValidator;
import com.onthegomap.planetiler.examples.BikeRouteOverlay;