2023-02-05 19:16:05 +00:00
|
|
|
package com.onthegomap.planetiler.archive;
|
2021-10-20 01:57:47 +00:00
|
|
|
|
2021-12-23 10:42:24 +00:00
|
|
|
import static com.onthegomap.planetiler.util.Gzip.gzip;
|
2022-02-24 01:45:56 +00:00
|
|
|
import static com.onthegomap.planetiler.worker.Worker.joinFutures;
|
2021-04-10 09:25:42 +00:00
|
|
|
|
2021-12-23 10:42:24 +00:00
|
|
|
import com.onthegomap.planetiler.VectorTile;
|
|
|
|
import com.onthegomap.planetiler.collection.FeatureGroup;
|
|
|
|
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
|
|
|
import com.onthegomap.planetiler.geo.TileCoord;
|
|
|
|
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.stats.Timer;
|
|
|
|
import com.onthegomap.planetiler.util.DiskBacked;
|
|
|
|
import com.onthegomap.planetiler.util.Format;
|
2023-01-14 21:03:50 +00:00
|
|
|
import com.onthegomap.planetiler.util.Hashing;
|
2023-12-15 00:26:27 +00:00
|
|
|
import com.onthegomap.planetiler.util.LayerAttrStats;
|
2023-09-22 01:44:09 +00:00
|
|
|
import com.onthegomap.planetiler.util.TileSizeStats;
|
|
|
|
import com.onthegomap.planetiler.util.TileWeights;
|
|
|
|
import com.onthegomap.planetiler.util.TilesetSummaryStatistics;
|
2021-12-23 10:42:24 +00:00
|
|
|
import com.onthegomap.planetiler.worker.WorkQueue;
|
2022-05-24 22:46:52 +00:00
|
|
|
import com.onthegomap.planetiler.worker.Worker;
|
2021-12-23 10:42:24 +00:00
|
|
|
import com.onthegomap.planetiler.worker.WorkerPipeline;
|
2021-04-12 10:54:52 +00:00
|
|
|
import java.io.IOException;
|
2023-09-22 01:44:09 +00:00
|
|
|
import java.nio.file.Path;
|
|
|
|
import java.text.NumberFormat;
|
2021-07-29 01:47:13 +00:00
|
|
|
import java.util.ArrayList;
|
2021-06-06 12:00:04 +00:00
|
|
|
import java.util.LinkedHashMap;
|
2021-07-29 01:47:13 +00:00
|
|
|
import java.util.List;
|
2023-09-22 01:44:09 +00:00
|
|
|
import java.util.Locale;
|
2021-06-06 12:00:04 +00:00
|
|
|
import java.util.Map;
|
2022-06-04 00:44:49 +00:00
|
|
|
import java.util.OptionalLong;
|
2021-07-26 00:49:58 +00:00
|
|
|
import java.util.concurrent.CompletableFuture;
|
|
|
|
import java.util.concurrent.ExecutionException;
|
2024-01-03 01:37:49 +00:00
|
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
2021-07-26 00:49:58 +00:00
|
|
|
import java.util.concurrent.atomic.AtomicReference;
|
2021-04-12 10:54:52 +00:00
|
|
|
import java.util.function.Consumer;
|
2022-03-19 09:46:03 +00:00
|
|
|
import java.util.function.LongSupplier;
|
2021-06-04 11:22:40 +00:00
|
|
|
import java.util.stream.IntStream;
|
2021-06-06 12:00:04 +00:00
|
|
|
import java.util.stream.Stream;
|
2021-04-12 10:54:52 +00:00
|
|
|
import org.slf4j.Logger;
|
|
|
|
import org.slf4j.LoggerFactory;
|
2021-04-10 09:25:42 +00:00
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/**
|
2023-01-17 12:05:45 +00:00
|
|
|
* Final stage of the map generation process that encodes vector tiles using {@link VectorTile} and writes them to a
|
2023-02-05 19:16:05 +00:00
|
|
|
* {@link WriteableTileArchive}.
|
2021-09-10 00:46:20 +00:00
|
|
|
*/
|
2023-01-17 12:05:45 +00:00
|
|
|
public class TileArchiveWriter {
|
2021-04-10 09:25:42 +00:00
|
|
|
|
2023-01-17 12:05:45 +00:00
|
|
|
private static final Logger LOGGER = LoggerFactory.getLogger(TileArchiveWriter.class);
|
2021-09-18 01:12:24 +00:00
|
|
|
private static final long MAX_FEATURES_PER_BATCH = 10_000;
|
|
|
|
private static final long MAX_TILES_PER_BATCH = 1_000;
|
2021-06-06 12:00:04 +00:00
|
|
|
private final Counter.Readable featuresProcessed;
|
|
|
|
private final Counter memoizedTiles;
|
2023-02-05 19:16:05 +00:00
|
|
|
private final WriteableTileArchive archive;
|
2021-12-23 10:42:24 +00:00
|
|
|
private final PlanetilerConfig config;
|
2021-04-12 10:54:52 +00:00
|
|
|
private final Stats stats;
|
2021-06-06 12:00:04 +00:00
|
|
|
private final Counter.Readable[] tilesByZoom;
|
2022-05-24 22:46:52 +00:00
|
|
|
private final Iterable<FeatureGroup.TileFeatures> inputTiles;
|
2021-07-26 00:49:58 +00:00
|
|
|
private final AtomicReference<TileCoord> lastTileWritten = new AtomicReference<>();
|
2023-01-17 12:05:45 +00:00
|
|
|
private final TileArchiveMetadata tileArchiveMetadata;
|
2023-09-22 01:44:09 +00:00
|
|
|
private final TilesetSummaryStatistics tileStats;
|
2023-12-15 00:26:27 +00:00
|
|
|
private final LayerAttrStats layerAttrStats = new LayerAttrStats();
|
2021-06-04 11:22:40 +00:00
|
|
|
|
2023-02-05 19:16:05 +00:00
|
|
|
private TileArchiveWriter(Iterable<FeatureGroup.TileFeatures> inputTiles, WriteableTileArchive archive,
|
2023-03-18 18:38:04 +00:00
|
|
|
PlanetilerConfig config, TileArchiveMetadata tileArchiveMetadata, Stats stats) {
|
2023-09-22 01:44:09 +00:00
|
|
|
this.tileStats = new TilesetSummaryStatistics(TileWeights.readFromFile(config.tileWeights()));
|
2022-05-24 22:46:52 +00:00
|
|
|
this.inputTiles = inputTiles;
|
2023-01-17 12:05:45 +00:00
|
|
|
this.archive = archive;
|
2021-05-01 20:08:20 +00:00
|
|
|
this.config = config;
|
2023-01-17 12:05:45 +00:00
|
|
|
this.tileArchiveMetadata = tileArchiveMetadata;
|
2021-04-12 10:54:52 +00:00
|
|
|
this.stats = stats;
|
2021-09-10 00:46:20 +00:00
|
|
|
tilesByZoom = IntStream.rangeClosed(0, config.maxzoom())
|
|
|
|
.mapToObj(i -> Counter.newSingleThreadCounter())
|
2021-06-06 12:00:04 +00:00
|
|
|
.toArray(Counter.Readable[]::new);
|
2023-01-17 12:05:45 +00:00
|
|
|
memoizedTiles = stats.longCounter("archive_memoized_tiles");
|
|
|
|
featuresProcessed = stats.longCounter("archive_features_processed");
|
2022-03-19 09:46:03 +00:00
|
|
|
Map<String, LongSupplier> countsByZoom = new LinkedHashMap<>();
|
2021-06-06 12:00:04 +00:00
|
|
|
for (int zoom = config.minzoom(); zoom <= config.maxzoom(); zoom++) {
|
|
|
|
countsByZoom.put(Integer.toString(zoom), tilesByZoom[zoom]);
|
|
|
|
}
|
2023-01-17 12:05:45 +00:00
|
|
|
stats.counter("archive_tiles_written", "zoom", () -> countsByZoom);
|
2021-05-08 10:53:37 +00:00
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/** Reads all {@code features}, encodes them in parallel, and writes to {@code output}. */
|
2023-02-05 19:16:05 +00:00
|
|
|
public static void writeOutput(FeatureGroup features, WriteableTileArchive output, DiskBacked fileSize,
|
2023-09-22 01:44:09 +00:00
|
|
|
TileArchiveMetadata tileArchiveMetadata, Path layerStatsPath, PlanetilerConfig config, Stats stats) {
|
2023-01-17 12:05:45 +00:00
|
|
|
var timer = stats.startStage("archive");
|
2022-05-24 22:46:52 +00:00
|
|
|
|
2024-01-14 17:08:20 +00:00
|
|
|
int chunksToRead = Math.max(1, features.chunksToRead());
|
|
|
|
int readThreads = Math.min(config.featureReadThreads(), chunksToRead);
|
2022-05-24 22:46:52 +00:00
|
|
|
int threads = config.threads();
|
2024-01-20 14:10:38 +00:00
|
|
|
int processThreads = threads < 8 ? threads : (threads - readThreads);
|
2023-08-24 00:24:27 +00:00
|
|
|
int tileWriteThreads = config.tileWriteThreads();
|
2022-05-24 22:46:52 +00:00
|
|
|
|
|
|
|
// when using more than 1 read thread: (N read threads) -> (1 merge thread) -> ...
|
|
|
|
// when using 1 read thread we just have: (1 read & merge thread) -> ...
|
|
|
|
Worker readWorker = null;
|
|
|
|
Iterable<FeatureGroup.TileFeatures> inputTiles;
|
|
|
|
String secondStageName;
|
|
|
|
if (readThreads == 1) {
|
|
|
|
secondStageName = "read";
|
|
|
|
inputTiles = features;
|
|
|
|
} else {
|
|
|
|
secondStageName = "merge";
|
|
|
|
var reader = features.parallelIterator(readThreads);
|
|
|
|
inputTiles = reader.result();
|
|
|
|
readWorker = reader.readWorker();
|
|
|
|
}
|
|
|
|
|
2023-12-15 00:26:27 +00:00
|
|
|
TileArchiveWriter writer = new TileArchiveWriter(inputTiles, output, config, tileArchiveMetadata, stats);
|
2021-04-12 10:54:52 +00:00
|
|
|
|
2023-01-17 12:05:45 +00:00
|
|
|
var pipeline = WorkerPipeline.start("archive", stats);
|
2021-07-26 00:49:58 +00:00
|
|
|
|
2021-10-20 01:57:47 +00:00
|
|
|
// a larger tile queue size helps keep cores busy, but needs a lot of RAM
|
|
|
|
// 5k works fine with 100GB of RAM, so adjust the queue size down from there
|
|
|
|
// but no less than 100
|
|
|
|
int queueSize = Math.max(
|
|
|
|
100,
|
|
|
|
(int) (5_000d * ProcessInfo.getMaxMemoryBytes() / 100_000_000_000d)
|
|
|
|
);
|
2021-07-26 00:49:58 +00:00
|
|
|
|
2023-01-27 13:12:54 +00:00
|
|
|
/*
|
|
|
|
* To emit tiles in order, fork the input queue and send features to both the encoder and writer. The writer
|
|
|
|
* waits on them to be encoded in the order they were received, and the encoder processes them in parallel.
|
|
|
|
* One batch might take a long time to process, so make the queues very big to avoid idle encoding CPUs.
|
2023-08-24 00:24:27 +00:00
|
|
|
*
|
|
|
|
* Note:
|
|
|
|
* In the future emitting tiles out order might be especially interesting when tileWriteThreads>1,
|
|
|
|
* since when multiple threads/files are included there's no order that needs to be preserved.
|
|
|
|
* So some of the restrictions could be lifted then.
|
2023-01-27 13:12:54 +00:00
|
|
|
*/
|
|
|
|
WorkQueue<TileBatch> writerQueue = new WorkQueue<>("archive_writer_queue", queueSize, 1, stats);
|
2023-09-22 01:44:09 +00:00
|
|
|
WorkQueue<TileBatch> layerStatsQueue = new WorkQueue<>("archive_layerstats_queue", queueSize, 1, stats);
|
|
|
|
WorkerPipeline<TileBatch> encodeBranch = pipeline
|
2023-01-27 13:12:54 +00:00
|
|
|
.<TileBatch>fromGenerator(secondStageName, next -> {
|
2023-09-22 01:44:09 +00:00
|
|
|
try (writerQueue; layerStatsQueue) {
|
|
|
|
var writerEnqueuer = writerQueue.threadLocalWriter();
|
|
|
|
var statsEnqueuer = layerStatsQueue.threadLocalWriter();
|
|
|
|
writer.readFeaturesAndBatch(batch -> {
|
|
|
|
next.accept(batch);
|
|
|
|
writerEnqueuer.accept(batch); // also send immediately to writer
|
|
|
|
if (config.outputLayerStats()) {
|
|
|
|
statsEnqueuer.accept(batch);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2021-09-10 00:46:20 +00:00
|
|
|
// use only 1 thread since readFeaturesAndBatch needs to be single-threaded
|
2023-01-27 13:12:54 +00:00
|
|
|
}, 1)
|
|
|
|
.addBuffer("reader_queue", queueSize)
|
|
|
|
.sinkTo("encode", processThreads, writer::tileEncoderSink);
|
|
|
|
|
2024-01-03 01:37:49 +00:00
|
|
|
// ensure to initialize the archive BEFORE starting to write any tiles
|
|
|
|
output.initialize();
|
|
|
|
|
2023-01-27 13:12:54 +00:00
|
|
|
// the tile writer will wait on the result of each batch to ensure tiles are written in order
|
2023-09-22 01:44:09 +00:00
|
|
|
WorkerPipeline<TileBatch> writeBranch = pipeline.readFromQueue(writerQueue)
|
2023-08-24 00:24:27 +00:00
|
|
|
.sinkTo("write", tileWriteThreads, writer::tileWriter);
|
2021-04-12 10:54:52 +00:00
|
|
|
|
2023-09-22 01:44:09 +00:00
|
|
|
WorkerPipeline<TileBatch> layerStatsBranch = null;
|
|
|
|
|
|
|
|
if (config.outputLayerStats()) {
|
|
|
|
layerStatsBranch = pipeline.readFromQueue(layerStatsQueue)
|
|
|
|
.sinkTo("stats", 1, tileStatsWriter(layerStatsPath));
|
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
var loggers = ProgressLoggers.create()
|
2022-03-01 01:52:30 +00:00
|
|
|
.addRatePercentCounter("features", features.numFeaturesWritten(), writer.featuresProcessed, true)
|
2021-10-20 01:57:47 +00:00
|
|
|
.addFileSize(features)
|
2021-06-06 12:00:04 +00:00
|
|
|
.addRateCounter("tiles", writer::tilesEmitted)
|
2021-05-08 10:53:37 +00:00
|
|
|
.addFileSize(fileSize)
|
2021-08-10 10:55:30 +00:00
|
|
|
.newLine()
|
2021-04-12 10:54:52 +00:00
|
|
|
.addProcessStats()
|
2022-05-24 22:46:52 +00:00
|
|
|
.newLine();
|
|
|
|
if (readWorker != null) {
|
|
|
|
loggers.addThreadPoolStats("read", readWorker);
|
|
|
|
}
|
|
|
|
loggers.addPipelineStats(encodeBranch)
|
2023-09-22 01:44:09 +00:00
|
|
|
.addPipelineStats(writeBranch);
|
|
|
|
if (layerStatsBranch != null) {
|
|
|
|
loggers.addPipelineStats(layerStatsBranch);
|
|
|
|
}
|
|
|
|
loggers.newLine()
|
2021-09-10 00:46:20 +00:00
|
|
|
.add(writer::getLastTileLogDetails);
|
2021-04-12 10:54:52 +00:00
|
|
|
|
2024-01-03 01:37:49 +00:00
|
|
|
final CompletableFuture<Void> tileWritersFuture = writeBranch.done();
|
|
|
|
final CompletableFuture<Void> layerStatsFuture =
|
|
|
|
layerStatsBranch == null ? CompletableFuture.completedFuture(null) : layerStatsBranch.done();
|
|
|
|
final CompletableFuture<Void> archiveFinisher =
|
|
|
|
CompletableFuture.allOf(tileWritersFuture, layerStatsFuture).thenRun(writer::finishArchive);
|
|
|
|
|
|
|
|
var doneFuture = joinFutures(tileWritersFuture, layerStatsFuture, encodeBranch.done(), archiveFinisher);
|
2022-02-24 01:45:56 +00:00
|
|
|
loggers.awaitAndLog(doneFuture, config.logInterval());
|
2021-07-26 00:49:58 +00:00
|
|
|
writer.printTileStats();
|
2021-06-08 00:55:23 +00:00
|
|
|
timer.stop();
|
2021-04-12 10:54:52 +00:00
|
|
|
}
|
|
|
|
|
2023-09-22 01:44:09 +00:00
|
|
|
private static WorkerPipeline.SinkStep<TileBatch> tileStatsWriter(Path layerStatsPath) {
|
|
|
|
return prev -> {
|
|
|
|
try (var statsWriter = TileSizeStats.newWriter(layerStatsPath)) {
|
|
|
|
statsWriter.write(TileSizeStats.headerRow());
|
|
|
|
for (var batch : prev) {
|
|
|
|
for (var encodedTile : batch.out().get()) {
|
|
|
|
for (var line : encodedTile.layerStats()) {
|
|
|
|
statsWriter.write(line);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
private String getLastTileLogDetails() {
|
|
|
|
TileCoord lastTile = lastTileWritten.get();
|
|
|
|
String blurb;
|
|
|
|
if (lastTile == null) {
|
|
|
|
blurb = "n/a";
|
|
|
|
} else {
|
2022-07-26 11:51:31 +00:00
|
|
|
blurb = "%d/%d/%d (z%d %s) %s".formatted(
|
2021-09-10 00:46:20 +00:00
|
|
|
lastTile.z(), lastTile.x(), lastTile.y(),
|
2023-01-27 02:43:07 +00:00
|
|
|
lastTile.z(),
|
|
|
|
Format.defaultInstance().percent(archive.tileOrder().progressOnLevel(lastTile, config.bounds().tileExtents())),
|
2023-09-22 01:44:09 +00:00
|
|
|
lastTile.getDebugUrl(config.debugUrlPattern())
|
2021-09-10 00:46:20 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
return "last tile: " + blurb;
|
|
|
|
}
|
|
|
|
|
|
|
|
private void readFeaturesAndBatch(Consumer<TileBatch> next) {
|
2021-07-18 09:57:48 +00:00
|
|
|
int currentZoom = Integer.MIN_VALUE;
|
2021-07-26 00:49:58 +00:00
|
|
|
TileBatch batch = new TileBatch();
|
2021-07-26 11:27:56 +00:00
|
|
|
long featuresInThisBatch = 0;
|
|
|
|
long tilesInThisBatch = 0;
|
2022-05-24 22:46:52 +00:00
|
|
|
for (var feature : inputTiles) {
|
2021-09-10 00:46:20 +00:00
|
|
|
int z = feature.tileCoord().z();
|
|
|
|
if (z != currentZoom) {
|
2022-04-23 09:58:49 +00:00
|
|
|
LOGGER.trace("Starting z{}", z);
|
2021-07-18 09:57:48 +00:00
|
|
|
currentZoom = z;
|
|
|
|
}
|
2021-07-29 11:07:58 +00:00
|
|
|
long thisTileFeatures = feature.getNumFeaturesToEmit();
|
2021-07-29 02:02:10 +00:00
|
|
|
if (tilesInThisBatch > 0 &&
|
|
|
|
(tilesInThisBatch >= MAX_TILES_PER_BATCH ||
|
|
|
|
((featuresInThisBatch + thisTileFeatures) > MAX_FEATURES_PER_BATCH))) {
|
2021-07-26 00:49:58 +00:00
|
|
|
next.accept(batch);
|
|
|
|
batch = new TileBatch();
|
2021-07-26 11:27:56 +00:00
|
|
|
featuresInThisBatch = 0;
|
|
|
|
tilesInThisBatch = 0;
|
2021-07-26 00:49:58 +00:00
|
|
|
}
|
2021-07-29 02:02:10 +00:00
|
|
|
featuresInThisBatch += thisTileFeatures;
|
2021-07-27 12:09:06 +00:00
|
|
|
tilesInThisBatch++;
|
2021-07-29 01:47:13 +00:00
|
|
|
batch.in.add(feature);
|
2021-07-26 00:49:58 +00:00
|
|
|
}
|
|
|
|
if (!batch.in.isEmpty()) {
|
|
|
|
next.accept(batch);
|
2021-07-18 09:57:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-24 01:32:41 +00:00
|
|
|
private void tileEncoderSink(Iterable<TileBatch> prev) throws IOException {
|
2021-09-10 00:46:20 +00:00
|
|
|
/*
|
|
|
|
* To optimize emitting many identical consecutive tiles (like large ocean areas), memoize output to avoid
|
|
|
|
* recomputing if the input hasn't changed.
|
|
|
|
*/
|
2021-04-12 10:54:52 +00:00
|
|
|
byte[] lastBytes = null, lastEncoded = null;
|
2022-06-04 00:44:49 +00:00
|
|
|
Long lastTileDataHash = null;
|
2022-06-04 01:04:17 +00:00
|
|
|
boolean lastIsFill = false;
|
2023-09-22 01:44:09 +00:00
|
|
|
List<TileSizeStats.LayerStats> lastLayerStats = null;
|
2022-06-04 01:04:17 +00:00
|
|
|
boolean skipFilled = config.skipFilledTiles();
|
2024-04-03 00:34:42 +00:00
|
|
|
var layerStatsSerializer = TileSizeStats.newThreadLocalSerializer();
|
2021-07-26 00:49:58 +00:00
|
|
|
|
2023-09-22 01:44:09 +00:00
|
|
|
var tileStatsUpdater = tileStats.threadLocalUpdater();
|
2023-12-15 00:26:27 +00:00
|
|
|
var layerAttrStatsUpdater = layerAttrStats.handlerForThread();
|
2022-02-24 01:32:41 +00:00
|
|
|
for (TileBatch batch : prev) {
|
2023-09-22 01:44:09 +00:00
|
|
|
List<TileEncodingResult> result = new ArrayList<>(batch.size());
|
2021-07-29 01:47:13 +00:00
|
|
|
FeatureGroup.TileFeatures last = null;
|
2023-09-22 01:44:09 +00:00
|
|
|
// each batch contains tile ordered by tile-order ID ascending
|
2021-07-29 01:47:13 +00:00
|
|
|
for (int i = 0; i < batch.in.size(); i++) {
|
|
|
|
FeatureGroup.TileFeatures tileFeatures = batch.in.get(i);
|
2021-07-29 11:07:58 +00:00
|
|
|
featuresProcessed.incBy(tileFeatures.getNumFeaturesProcessed());
|
2021-07-26 00:49:58 +00:00
|
|
|
byte[] bytes, encoded;
|
2023-09-22 01:44:09 +00:00
|
|
|
List<TileSizeStats.LayerStats> layerStats;
|
2022-06-04 00:44:49 +00:00
|
|
|
Long tileDataHash;
|
2021-07-26 00:49:58 +00:00
|
|
|
if (tileFeatures.hasSameContents(last)) {
|
|
|
|
bytes = lastBytes;
|
|
|
|
encoded = lastEncoded;
|
2022-05-24 21:46:56 +00:00
|
|
|
tileDataHash = lastTileDataHash;
|
2023-09-22 01:44:09 +00:00
|
|
|
layerStats = lastLayerStats;
|
2021-07-26 00:49:58 +00:00
|
|
|
memoizedTiles.inc();
|
|
|
|
} else {
|
2023-12-15 00:26:27 +00:00
|
|
|
VectorTile tile = tileFeatures.getVectorTile(layerAttrStatsUpdater);
|
2023-09-24 12:10:47 +00:00
|
|
|
if (skipFilled && (lastIsFill = tile.containsOnlyFills())) {
|
2022-06-20 11:32:55 +00:00
|
|
|
encoded = null;
|
2023-09-22 01:44:09 +00:00
|
|
|
layerStats = null;
|
2022-06-20 11:32:55 +00:00
|
|
|
bytes = null;
|
|
|
|
} else {
|
2023-09-24 12:10:47 +00:00
|
|
|
var proto = tile.toProto();
|
2023-09-22 01:44:09 +00:00
|
|
|
encoded = proto.toByteArray();
|
2023-08-24 00:24:27 +00:00
|
|
|
bytes = switch (config.tileCompression()) {
|
|
|
|
case GZIP -> gzip(encoded);
|
|
|
|
case NONE -> encoded;
|
2024-01-10 10:21:03 +00:00
|
|
|
case UNKNOWN -> throw new IllegalArgumentException("cannot compress \"UNKNOWN\"");
|
2023-08-24 00:24:27 +00:00
|
|
|
};
|
2023-09-22 01:44:09 +00:00
|
|
|
layerStats = TileSizeStats.computeTileStats(proto);
|
2022-07-14 09:26:53 +00:00
|
|
|
if (encoded.length > config.tileWarningSizeBytes()) {
|
2022-06-20 11:32:55 +00:00
|
|
|
LOGGER.warn("{} {}kb uncompressed",
|
|
|
|
tileFeatures.tileCoord(),
|
|
|
|
encoded.length / 1024);
|
2022-06-04 01:04:17 +00:00
|
|
|
}
|
|
|
|
}
|
2023-09-22 01:44:09 +00:00
|
|
|
lastLayerStats = layerStats;
|
2022-06-20 11:32:55 +00:00
|
|
|
lastEncoded = encoded;
|
|
|
|
lastBytes = bytes;
|
2021-07-26 00:49:58 +00:00
|
|
|
last = tileFeatures;
|
2023-09-24 12:10:47 +00:00
|
|
|
if (archive.deduplicates() && tile.likelyToBeDuplicated() && bytes != null) {
|
2023-01-14 21:03:50 +00:00
|
|
|
tileDataHash = generateContentHash(bytes);
|
2022-05-24 21:46:56 +00:00
|
|
|
} else {
|
|
|
|
tileDataHash = null;
|
|
|
|
}
|
|
|
|
lastTileDataHash = tileDataHash;
|
2021-04-12 10:54:52 +00:00
|
|
|
}
|
2023-09-22 01:44:09 +00:00
|
|
|
if ((!skipFilled || !lastIsFill) && bytes != null) {
|
|
|
|
tileStatsUpdater.recordTile(tileFeatures.tileCoord(), bytes.length, layerStats);
|
|
|
|
List<String> layerStatsRows = config.outputLayerStats() ?
|
2024-04-03 00:34:42 +00:00
|
|
|
layerStatsSerializer.formatOutputRows(tileFeatures.tileCoord(), bytes.length, layerStats) :
|
2023-09-22 01:44:09 +00:00
|
|
|
List.of();
|
|
|
|
result.add(
|
|
|
|
new TileEncodingResult(
|
|
|
|
tileFeatures.tileCoord(),
|
|
|
|
bytes,
|
|
|
|
encoded.length,
|
|
|
|
tileDataHash == null ? OptionalLong.empty() : OptionalLong.of(tileDataHash),
|
|
|
|
layerStatsRows
|
|
|
|
)
|
|
|
|
);
|
2022-06-20 11:32:55 +00:00
|
|
|
}
|
2021-04-12 10:54:52 +00:00
|
|
|
}
|
2021-09-10 00:46:20 +00:00
|
|
|
// hand result off to writer
|
2021-07-26 00:49:58 +00:00
|
|
|
batch.out.complete(result);
|
2021-04-12 10:54:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-03 01:37:49 +00:00
|
|
|
private final AtomicBoolean firstTileWriterTracker = new AtomicBoolean(true);
|
|
|
|
|
2022-02-24 01:32:41 +00:00
|
|
|
private void tileWriter(Iterable<TileBatch> tileBatches) throws ExecutionException, InterruptedException {
|
2024-01-03 01:37:49 +00:00
|
|
|
|
|
|
|
final boolean firstTileWriter = firstTileWriterTracker.compareAndExchange(true, false);
|
|
|
|
|
2023-09-22 01:44:09 +00:00
|
|
|
var f = NumberFormat.getNumberInstance(Locale.getDefault());
|
|
|
|
f.setMaximumFractionDigits(5);
|
2022-06-02 01:29:59 +00:00
|
|
|
|
2023-03-18 18:38:04 +00:00
|
|
|
var order = archive.tileOrder();
|
2023-01-02 16:26:00 +00:00
|
|
|
|
2021-07-26 00:49:58 +00:00
|
|
|
TileCoord lastTile = null;
|
2021-09-10 00:46:20 +00:00
|
|
|
Timer time = null;
|
|
|
|
int currentZ = Integer.MIN_VALUE;
|
2023-01-17 12:05:45 +00:00
|
|
|
try (var tileWriter = archive.newTileWriter()) {
|
2022-02-24 01:32:41 +00:00
|
|
|
for (TileBatch batch : tileBatches) {
|
2023-09-22 01:44:09 +00:00
|
|
|
for (var encodedTile : batch.out.get()) {
|
2022-05-24 21:46:56 +00:00
|
|
|
TileCoord tileCoord = encodedTile.coord();
|
2023-03-18 18:38:04 +00:00
|
|
|
assert lastTile == null ||
|
|
|
|
order.encode(tileCoord) > order.encode(lastTile) : "Tiles out of order %s before %s"
|
|
|
|
.formatted(lastTile, tileCoord);
|
2022-05-24 21:46:56 +00:00
|
|
|
lastTile = encodedTile.coord();
|
2021-07-26 00:49:58 +00:00
|
|
|
int z = tileCoord.z();
|
2021-09-10 00:46:20 +00:00
|
|
|
if (z != currentZ) {
|
2024-01-03 01:37:49 +00:00
|
|
|
// for multiple writers the starting/finish log message of the _first_ tilewriter
|
|
|
|
// is not 100% accurate in terms of overall "zoom-progress",
|
|
|
|
// but it should be a "good-enough" indicator for "zoom-progress"-logging
|
|
|
|
if (firstTileWriter) {
|
|
|
|
if (time == null) {
|
|
|
|
LOGGER.info("Starting z{}", z);
|
|
|
|
} else {
|
|
|
|
LOGGER.info("Finished z{} in {}, now starting z{}", currentZ, time.stop(), z);
|
|
|
|
}
|
2021-09-10 00:46:20 +00:00
|
|
|
}
|
|
|
|
time = Timer.start();
|
|
|
|
currentZ = z;
|
|
|
|
}
|
2023-01-17 12:05:45 +00:00
|
|
|
tileWriter.write(encodedTile);
|
2022-05-24 21:46:56 +00:00
|
|
|
|
2023-09-22 01:44:09 +00:00
|
|
|
stats.wroteTile(z, encodedTile.tileData().length);
|
2021-07-26 00:49:58 +00:00
|
|
|
tilesByZoom[z].inc();
|
|
|
|
}
|
|
|
|
lastTileWritten.set(lastTile);
|
2021-05-01 20:08:20 +00:00
|
|
|
}
|
2023-01-17 12:05:45 +00:00
|
|
|
tileWriter.printStats();
|
2021-05-08 10:53:37 +00:00
|
|
|
}
|
2021-05-01 20:08:20 +00:00
|
|
|
|
2021-10-20 01:57:47 +00:00
|
|
|
if (time != null) {
|
2022-04-23 09:58:49 +00:00
|
|
|
LOGGER.info("Finished z{} in {}", currentZ, time.stop());
|
2021-10-20 01:57:47 +00:00
|
|
|
}
|
2021-06-04 11:22:40 +00:00
|
|
|
}
|
|
|
|
|
2023-09-22 01:44:09 +00:00
|
|
|
@SuppressWarnings("java:S2629")
|
2021-06-04 11:22:40 +00:00
|
|
|
private void printTileStats() {
|
2023-09-22 01:44:09 +00:00
|
|
|
Format format = Format.defaultInstance();
|
|
|
|
tileStats.printStats(config.debugUrlPattern());
|
|
|
|
LOGGER.debug(" # features: {}", format.integer(featuresProcessed.get()));
|
2021-06-06 12:00:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private long tilesEmitted() {
|
2021-09-10 00:46:20 +00:00
|
|
|
return Stream.of(tilesByZoom).mapToLong(c -> c.get()).sum();
|
2021-04-12 10:54:52 +00:00
|
|
|
}
|
|
|
|
|
2024-01-03 01:37:49 +00:00
|
|
|
private void finishArchive() {
|
|
|
|
archive.finish(tileArchiveMetadata.withLayerStats(layerAttrStats.getTileStats()));
|
|
|
|
}
|
|
|
|
|
2023-01-14 21:03:50 +00:00
|
|
|
/**
|
|
|
|
* Generates a hash over encoded and compressed tile.
|
|
|
|
* <p>
|
|
|
|
* Used as an optimization to avoid writing the same (mostly ocean) tiles over and over again.
|
|
|
|
*/
|
|
|
|
public static long generateContentHash(byte[] bytes) {
|
|
|
|
return Hashing.fnv1a64(bytes);
|
|
|
|
}
|
|
|
|
|
2021-09-18 01:12:24 +00:00
|
|
|
/**
|
|
|
|
* Container for a batch of tiles to be processed together in the encoder and writer threads.
|
|
|
|
* <p>
|
|
|
|
* The cost of encoding a tile may vary dramatically by its size (depending on the profile) so batches are sized
|
|
|
|
* dynamically to put as little as 1 large tile, or as many as 10,000 small tiles in a batch to keep encoding threads
|
|
|
|
* busy.
|
|
|
|
*
|
|
|
|
* @param in the tile data to encode
|
|
|
|
* @param out the future that encoder thread completes to hand finished tile off to writer thread
|
|
|
|
*/
|
2022-02-24 01:45:56 +00:00
|
|
|
private record TileBatch(
|
2021-09-18 01:12:24 +00:00
|
|
|
List<FeatureGroup.TileFeatures> in,
|
2023-09-22 01:44:09 +00:00
|
|
|
CompletableFuture<List<TileEncodingResult>> out
|
2021-09-18 01:12:24 +00:00
|
|
|
) {
|
|
|
|
|
|
|
|
TileBatch() {
|
|
|
|
this(new ArrayList<>(), new CompletableFuture<>());
|
|
|
|
}
|
|
|
|
|
|
|
|
public int size() {
|
|
|
|
return in.size();
|
|
|
|
}
|
|
|
|
|
|
|
|
public boolean isEmpty() {
|
|
|
|
return in.isEmpty();
|
2021-04-12 10:54:52 +00:00
|
|
|
}
|
2021-04-10 09:25:42 +00:00
|
|
|
}
|
|
|
|
}
|