planetiler/planetiler-core/src/main/java/com/onthegomap/planetiler/util/LayerStats.java

181 wiersze
6.4 KiB
Java
Czysty Zwykły widok Historia

package com.onthegomap.planetiler.util;
2021-05-13 10:25:06 +00:00
import com.fasterxml.jackson.annotation.JsonProperty;
import com.onthegomap.planetiler.archive.WriteableTileArchive;
import com.onthegomap.planetiler.mbtiles.Mbtiles;
import com.onthegomap.planetiler.render.RenderedFeature;
2021-05-13 10:25:06 +00:00
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
2021-05-13 10:25:06 +00:00
import java.util.TreeMap;
2021-08-11 12:40:49 +00:00
import java.util.concurrent.CopyOnWriteArrayList;
2021-05-13 10:25:06 +00:00
import java.util.function.Consumer;
2021-09-10 00:46:20 +00:00
import javax.annotation.concurrent.NotThreadSafe;
import javax.annotation.concurrent.ThreadSafe;
2021-05-13 10:25:06 +00:00
2021-09-10 00:46:20 +00:00
/**
* Tracks the feature attributes and zoom range of each layer to populate the archive output metadata.
2021-09-10 00:46:20 +00:00
* <p>
* Matches the MBTiles spec for {@code vector_layers}, but can be reused by other {@link WriteableTileArchive} formats.
* To minimize overhead of stat collection, each updating thread should call {@link #handlerForThread()} first to get a
2021-09-10 00:46:20 +00:00
* thread-local handler that can update stats without contention.
* </p>
2021-09-10 00:46:20 +00:00
*
* @see Mbtiles.MetadataJson
2021-10-20 01:57:47 +00:00
* @see <a href="https://github.com/mapbox/mbtiles-spec/blob/master/1.3/spec.md#content">MBtiles spec</a>
2021-09-10 00:46:20 +00:00
*/
@ThreadSafe
2021-05-13 10:25:06 +00:00
public class LayerStats implements Consumer<RenderedFeature> {
2021-09-10 00:46:20 +00:00
/*
* This utility is called for billions of features by multiple threads when processing the planet which can make
* access to shared data structures a bottleneck. So give each thread an individual ThreadLocalLayerStatsHandler to
* update and aggregate the results on read.
*/
2021-05-13 10:25:06 +00:00
2021-09-10 00:46:20 +00:00
private final List<ThreadLocalHandler> threadLocals = new CopyOnWriteArrayList<>();
2022-04-27 11:24:14 +00:00
// Ignore warnings about not removing thread local values since planetiler uses dedicated worker threads that release
// values when a task is finished and are not re-used.
@SuppressWarnings("java:S5164")
2021-09-10 00:46:20 +00:00
private final ThreadLocal<ThreadLocalHandler> layerStats = ThreadLocal
.withInitial(ThreadLocalHandler::new);
2021-05-13 10:25:06 +00:00
/** Returns stats on all features that have been emitted as a list of {@link VectorLayer} objects. */
public List<VectorLayer> getTileStats() {
2021-09-10 00:46:20 +00:00
Map<String, StatsForLayer> layers = new TreeMap<>();
2021-08-11 12:40:49 +00:00
for (var threadLocal : threadLocals) {
2021-09-10 00:46:20 +00:00
for (StatsForLayer stats : threadLocal.layers.values()) {
layers.merge(stats.layer, stats, (prev, next) -> {
prev.expandZoomRangeToInclude(next.maxzoom);
prev.expandZoomRangeToInclude(next.minzoom);
for (var entry : next.fields.entrySet()) {
// keep track of field type as a number/boolean/string but widen to string if multiple different
// types are encountered
prev.fields.merge(entry.getKey(), entry.getValue(), FieldType::merge);
2021-08-11 12:40:49 +00:00
}
2021-09-10 00:46:20 +00:00
return prev;
2021-08-11 12:40:49 +00:00
});
}
2021-05-13 10:25:06 +00:00
}
return layers.values().stream()
.map(stats -> new VectorLayer(stats.layer, stats.fields, stats.minzoom, stats.maxzoom))
.toList();
}
public enum FieldType {
@JsonProperty("Number")
NUMBER,
@JsonProperty("Boolean")
BOOLEAN,
@JsonProperty("String")
STRING;
/**
* Per the MBTiles spec: attributes whose type varies between features SHOULD be listed as "String"
*/
public static FieldType merge(FieldType oldValue, FieldType newValue) {
return oldValue != newValue ? STRING : newValue;
}
}
public record VectorLayer(
@JsonProperty("id") String id,
@JsonProperty("fields") Map<String, FieldType> fields,
@JsonProperty("description") Optional<String> description,
@JsonProperty("minzoom") OptionalInt minzoom,
@JsonProperty("maxzoom") OptionalInt maxzoom
) {
public VectorLayer(String id, Map<String, FieldType> fields) {
this(id, fields, Optional.empty(), OptionalInt.empty(), OptionalInt.empty());
}
public VectorLayer(String id, Map<String, FieldType> fields, int minzoom, int maxzoom) {
this(id, fields, Optional.empty(), OptionalInt.of(minzoom), OptionalInt.of(maxzoom));
}
public static VectorLayer forLayer(String id) {
return new VectorLayer(id, new HashMap<>());
}
public VectorLayer withDescription(String newDescription) {
return new VectorLayer(id, fields, Optional.of(newDescription), minzoom, maxzoom);
}
public VectorLayer withMinzoom(int newMinzoom) {
return new VectorLayer(id, fields, description, OptionalInt.of(newMinzoom), maxzoom);
}
public VectorLayer withMaxzoom(int newMaxzoom) {
return new VectorLayer(id, fields, description, minzoom, OptionalInt.of(newMaxzoom));
}
2021-05-13 10:25:06 +00:00
}
2021-09-10 00:46:20 +00:00
/** Accepts features from a single thread that will be combined across all threads in {@link #getTileStats()}. */
@NotThreadSafe
private class ThreadLocalHandler implements Consumer<RenderedFeature> {
2021-08-11 12:40:49 +00:00
2021-09-10 00:46:20 +00:00
private final Map<String, StatsForLayer> layers = new TreeMap<>();
2021-08-11 12:40:49 +00:00
2021-09-10 00:46:20 +00:00
ThreadLocalHandler() {
2021-08-11 12:40:49 +00:00
threadLocals.add(this);
}
@Override
public void accept(RenderedFeature feature) {
var vectorTileFeature = feature.vectorTileFeature();
2021-09-10 00:46:20 +00:00
var stats = layers.computeIfAbsent(vectorTileFeature.layer(), StatsForLayer::new);
stats.expandZoomRangeToInclude(feature.tile().z());
for (var entry : vectorTileFeature.attrs().entrySet()) {
2021-08-11 12:40:49 +00:00
String key = entry.getKey();
Object value = entry.getValue();
FieldType fieldType = null;
2021-08-11 12:40:49 +00:00
if (value instanceof Number) {
fieldType = FieldType.NUMBER;
2021-08-11 12:40:49 +00:00
} else if (value instanceof Boolean) {
fieldType = FieldType.BOOLEAN;
2021-08-11 12:40:49 +00:00
} else if (value != null) {
fieldType = FieldType.STRING;
2021-08-11 12:40:49 +00:00
}
if (fieldType != null) {
2021-09-10 00:46:20 +00:00
// widen different types to string
stats.fields.merge(key, fieldType, FieldType::merge);
2021-05-13 10:25:06 +00:00
}
}
}
}
2021-09-10 00:46:20 +00:00
/**
* Returns a handler optimized for accepting features from a single thread.
* <p>
* Use this instead of {@link #accept(RenderedFeature)}
*/
2021-08-11 12:40:49 +00:00
public Consumer<RenderedFeature> handlerForThread() {
return layerStats.get();
}
2021-05-13 10:25:06 +00:00
@Override
public void accept(RenderedFeature feature) {
2021-08-11 12:40:49 +00:00
handlerForThread().accept(feature);
2021-05-13 10:25:06 +00:00
}
2021-09-10 00:46:20 +00:00
private static class StatsForLayer {
2021-05-13 10:25:06 +00:00
private final String layer;
private final Map<String, FieldType> fields = new HashMap<>();
2021-05-13 10:25:06 +00:00
private int minzoom = Integer.MAX_VALUE;
private int maxzoom = Integer.MIN_VALUE;
2021-09-10 00:46:20 +00:00
private StatsForLayer(String layer) {
2021-05-13 10:25:06 +00:00
this.layer = layer;
}
2021-09-10 00:46:20 +00:00
private void expandZoomRangeToInclude(int zoom) {
2021-05-13 10:25:06 +00:00
minzoom = Math.min(zoom, minzoom);
maxzoom = Math.max(zoom, maxzoom);
}
}
}