planetiler/planetiler-core/src/main/java/com/onthegomap/planetiler/FeatureCollector.java

731 wiersze
29 KiB
Java

package com.onthegomap.planetiler;
import com.onthegomap.planetiler.collection.FeatureGroup;
import com.onthegomap.planetiler.config.PlanetilerConfig;
import com.onthegomap.planetiler.geo.GeoUtils;
import com.onthegomap.planetiler.geo.GeometryException;
import com.onthegomap.planetiler.geo.GeometryType;
import com.onthegomap.planetiler.reader.SourceFeature;
import com.onthegomap.planetiler.render.FeatureRenderer;
import com.onthegomap.planetiler.stats.Stats;
import com.onthegomap.planetiler.util.CacheByZoom;
import com.onthegomap.planetiler.util.ZoomFunction;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.locationtech.jts.geom.Geometry;
/**
* Utility that {@link Profile} implementations use to build map features that should be emitted for an input source
* feature.
* <p>
* For example to add a polygon feature for a lake and a center label point with its name:
*
* <pre>
* {@code
* featureCollector.polygon("water")
* .setAttr("class", "lake");
* featureCollector.centroid("water_name")
* .setAttr("class", "lake")
* .setAttr("name", element.getString("name"));
* }
* </pre>
*/
public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
private static final Geometry EMPTY_GEOM = GeoUtils.JTS_FACTORY.createGeometryCollection();
private final SourceFeature source;
private final List<Feature> output = new ArrayList<>();
private final PlanetilerConfig config;
private final Stats stats;
private FeatureCollector(SourceFeature source, PlanetilerConfig config, Stats stats) {
this.source = source;
this.config = config;
this.stats = stats;
}
@Override
public Iterator<Feature> iterator() {
return output.iterator();
}
/**
* Starts building a new map feature with an explicit JTS {@code geometry} that overrides the source geometry.
*
* @param layer the output vector tile layer this feature will be written to
* @param geometry the explicit geometry to use instead of what is present in source data
* @return a feature that can be configured further.
*/
public Feature geometry(String layer, Geometry geometry) {
Feature feature = new Feature(layer, geometry, source.id());
output.add(feature);
return feature;
}
/**
* Starts building a new point map feature that expects the source feature to be a point.
* <p>
* If the source feature is not a point, logs an error and returns a feature that can be configured, but won't
* actually emit anything to the map.
*
* @param layer the output vector tile layer this feature will be written to
* @return a feature that can be configured further.
*/
public Feature point(String layer) {
try {
if (!source.isPoint()) {
throw new GeometryException("feature_not_point", "not a point");
}
return geometry(layer, source.worldGeometry());
} catch (GeometryException e) {
e.log(stats, "feature_point", "Error getting point geometry for " + source.id());
return new Feature(layer, EMPTY_GEOM, source.id());
}
}
/**
* Starts building a new line map feature that expects the source feature to be a line.
* <p>
* If the source feature cannot be a line, logs an error and returns a feature that can be configured, but won't
* actually emit anything to the map.
* <p>
* Some OSM closed OSM ways can be both a polygon and a line
*
* @param layer the output vector tile layer this feature will be written to
* @return a feature that can be configured further.
*/
public Feature line(String layer) {
try {
return geometry(layer, source.line());
} catch (GeometryException e) {
e.log(stats, "feature_line", "Error constructing line for " + source.id());
return new Feature(layer, EMPTY_GEOM, source.id());
}
}
/**
* Starts building a new polygon map feature that expects the source feature to be a polygon.
* <p>
* If the source feature cannot be a polygon, logs an error and returns a feature that can be configured, but won't
* actually emit anything to the map.
* <p>
* Some OSM closed OSM ways can be both a polygon and a line
*
* @param layer the output vector tile layer this feature will be written to
* @return a feature that can be configured further.
*/
public Feature polygon(String layer) {
try {
return geometry(layer, source.polygon());
} catch (GeometryException e) {
e.log(stats, "feature_polygon", "Error constructing polygon for " + source.id());
return new Feature(layer, EMPTY_GEOM, source.id());
}
}
/**
* Starts building a new point map feature with geometry from {@link Geometry#getCentroid()} of the source feature.
*
* @param layer the output vector tile layer this feature will be written to
* @return a feature that can be configured further.
*/
public Feature centroid(String layer) {
try {
return geometry(layer, source.centroid());
} catch (GeometryException e) {
e.log(stats, "feature_centroid", "Error getting centroid for " + source.id());
return new Feature(layer, EMPTY_GEOM, source.id());
}
}
/**
* Starts building a new point map feature with geometry from {@link Geometry#getCentroid()} if the source feature is
* a point, line, or simple convex polygon, or {@link Geometry#getInteriorPoint()} if it is a multipolygon, polygon
* with holes, or concave simple polygon.
*
* @param layer the output vector tile layer this feature will be written to
* @return a feature that can be configured further.
*/
public Feature centroidIfConvex(String layer) {
try {
return geometry(layer, source.centroidIfConvex());
} catch (GeometryException e) {
e.log(stats, "feature_centroid_if_convex", "Error constructing centroid if convex for " + source.id());
return new Feature(layer, EMPTY_GEOM, source.id());
}
}
/**
* Starts building a new point map feature with geometry from {@link Geometry#getInteriorPoint()} of the source
* feature.
*
* @param layer the output vector tile layer this feature will be written to
* @return a feature that can be configured further.
*/
public Feature pointOnSurface(String layer) {
try {
return geometry(layer, source.pointOnSurface());
} catch (GeometryException e) {
e.log(stats, "feature_point_on_surface", "Error constructing point on surface for " + source.id());
return new Feature(layer, EMPTY_GEOM, source.id());
}
}
/**
* Creates new feature collector instances for each source feature that we encounter.
*/
public record Factory(PlanetilerConfig config, Stats stats) {
public FeatureCollector get(SourceFeature source) {
return new FeatureCollector(source, config, stats);
}
}
/**
* A builder for an output map feature that contains all the information that will be needed to render vector tile
* features from the input element.
* <p>
* Some feature attributes are set globally (like sort key), and some allow the value to change by zoom-level (like
* tags).
*/
public final class Feature {
private static final double DEFAULT_LABEL_GRID_SIZE = 0;
private static final int DEFAULT_LABEL_GRID_LIMIT = 0;
private final String layer;
private final Geometry geom;
private final Map<String, Object> attrs = new TreeMap<>();
private final GeometryType geometryType;
private final long sourceId;
private int sortKey = 0;
private int minzoom = config.minzoom();
private int maxzoom = config.maxzoom();
private ZoomFunction<Number> labelGridPixelSize = null;
private ZoomFunction<Number> labelGridLimit = null;
private boolean attrsChangeByZoom = false;
private CacheByZoom<Map<String, Object>> attrCache = null;
private double defaultBufferPixels = 4;
private ZoomFunction<Number> bufferPixelOverrides;
// TODO better API for default value, value at max zoom, and zoom-specific overrides for tolerance and min size?
private double defaultMinPixelSize = config.minFeatureSizeBelowMaxZoom();
private double minPixelSizeAtMaxZoom = config.minFeatureSizeAtMaxZoom();
private ZoomFunction<Number> minPixelSize = null;
private double defaultPixelTolerance = config.simplifyToleranceBelowMaxZoom();
private double pixelToleranceAtMaxZoom = config.simplifyToleranceAtMaxZoom();
private ZoomFunction<Number> pixelTolerance = null;
private String numPointsAttr = null;
private Feature(String layer, Geometry geom, long sourceId) {
this.layer = layer;
this.geom = geom;
this.geometryType = GeometryType.valueOf(geom);
this.sourceId = sourceId;
}
/** Returns the original ID of the source feature that this feature came from (i.e. OSM node/way ID). */
public long getSourceId() {
return sourceId;
}
GeometryType getGeometryType() {
return geometryType;
}
public boolean isPolygon() {
return geometryType == GeometryType.POLYGON;
}
/**
* Returns the value by which features are sorted within a layer in the output vector tile.
*/
public int getSortKey() {
return sortKey;
}
/**
* Sets the value by which features are sorted within a layer in the output vector tile. Sort key gets packed into
* {@link FeatureGroup#SORT_KEY_BITS} bits so the range of this is limited to {@code -(2^(bits-1))} to {@code
* (2^(bits-1))-1}.
* <p>
* Circles, lines, and polygons are rendered in the order they appear in each layer, so features that appear later
* (higher sort key) show up on top of features with a lower sort key.
* <p>
* For symbols (text/icons) where clients try to avoid label collisions, features are placed in the order they
* appear in each layer, so features that appear earlier (lower sort key) will show up at lower zoom levels than
* feature that appear later (higher sort key) in a layer.
*/
public Feature setSortKey(int sortKey) {
assert sortKey >= FeatureGroup.SORT_KEY_MIN && sortKey <= FeatureGroup.SORT_KEY_MAX : "Sort key " + sortKey +
" outside of allowed range [" + FeatureGroup.SORT_KEY_MIN + ", " + FeatureGroup.SORT_KEY_MAX + "]";
this.sortKey = sortKey;
return this;
}
/** Sets the value by which features are sorted from high to low within a layer in the output vector tile. */
public Feature setSortKeyDescending(int sortKey) {
return setSortKey(FeatureGroup.SORT_KEY_MAX + FeatureGroup.SORT_KEY_MIN - sortKey);
}
/**
* Sets the zoom range (inclusive) that this feature appears in.
* <p>
* If not called, then defaults to all zoom levels.
*/
public Feature setZoomRange(int min, int max) {
assert min <= max;
return setMinZoom(min).setMaxZoom(max);
}
/** Returns the minimum zoom level (inclusive) that this feature appears in. */
public int getMinZoom() {
return minzoom;
}
/**
* Sets the minimum zoom level (inclusive) that this feature appears in.
* <p>
* If not called, defaults to minimum zoom-level of the map.
*/
public Feature setMinZoom(int min) {
minzoom = Math.max(min, config.minzoom());
return this;
}
/** Returns the maximum zoom level (inclusive) that this feature appears in. */
public int getMaxZoom() {
return maxzoom;
}
/**
* Sets the maximum zoom level (inclusive) that this feature appears in.
* <p>
* If not called, defaults to maximum zoom-level of the map.
*/
public Feature setMaxZoom(int max) {
maxzoom = Math.min(max, config.maxzoom());
return this;
}
/** Returns the output vector tile layer that this feature will appear in. */
public String getLayer() {
return layer;
}
/**
* Returns the JTS geometry (in world web mercator coordinates) of this feature.
* <p>
* Subsequent postprocessing in {@link FeatureRenderer} will slice this into tile geometries.
*/
public Geometry getGeometry() {
return geom;
}
/** Returns the number of pixels of detail to render outside the visible tile boundary at {@code zoom}. */
public double getBufferPixelsAtZoom(int zoom) {
return ZoomFunction.applyAsDoubleOrElse(bufferPixelOverrides, zoom, defaultBufferPixels);
}
/**
* Sets the default number of pixels of detail to render outside the visible tile boundary when no zoom-specific
* override is set in {@link #setBufferPixelOverrides(ZoomFunction)}.
*/
public Feature setBufferPixels(double buffer) {
defaultBufferPixels = buffer;
return this;
}
/**
* Sets zoom-specific overrides to the number of pixels of detail to render outside the visible tile boundary.
* <p>
* If {@code buffer} is {@code null} or returns {@code null}, the buffer pixels will default to
* {@link #setBufferPixels(double)}.
*/
public Feature setBufferPixelOverrides(ZoomFunction<Number> buffer) {
bufferPixelOverrides = buffer;
return this;
}
/**
* Returns the minimum resolution in tile pixels of features to emit at {@code zoom}.
* <p>
* For line features, this is length, and for polygon features this is the square root of the minimum area of
* features to emit.
*/
public double getMinPixelSizeAtZoom(int zoom) {
return zoom == config.maxzoom() ? minPixelSizeAtMaxZoom :
ZoomFunction.applyAsDoubleOrElse(minPixelSize, zoom, defaultMinPixelSize);
}
/**
* Sets the minimum length of line features or square root of the minimum area of polygon features to emit below the
* maximum zoom-level of the map.
* <p>
* At the maximum zoom level of the map, clients can "overzoom" in on features, so this leaves the minimum size at
* the max zoom level at {@link PlanetilerConfig#minFeatureSizeAtMaxZoom()} unless you explicitly override it with
* {@link #setMinPixelSizeAtMaxZoom(double)} or {@link #setMinPixelSizeAtAllZooms(int)}.
*/
public Feature setMinPixelSize(double minPixelSize) {
this.defaultMinPixelSize = minPixelSize;
return this;
}
/**
* Sets zoom-specific overrides to the minimum length of line features or square root of the minimum area of polygon
* features to emit below the maximum zoom-level of the map.
* <p>
* At the maximum zoom level of the map, clients can "overzoom" in on features, so this leaves the minimum size at
* the max zoom level at {@link PlanetilerConfig#minFeatureSizeAtMaxZoom()} unless you explicitly override it with
* {@link #setMinPixelSizeAtMaxZoom(double)} or {@link #setMinPixelSizeAtAllZooms(int)}.
* <p>
* If {@code levels} is {@code null} or returns {@code null}, the min pixel size will default to the default value.
*/
public Feature setMinPixelSizeOverrides(ZoomFunction<Number> levels) {
this.minPixelSize = levels;
return this;
}
/**
* Overrides the default minimum pixel size at and below {@code zoom} with {@code minPixelSize}.
* <p>
* This replaces all previous zoom overrides that were set. To use multiple zoom-level thresholds, create a
* {@link ZoomFunction} explicitly and pass it to {@link #setMinPixelSizeOverrides(ZoomFunction)}.
*/
public Feature setMinPixelSizeBelowZoom(int zoom, double minPixelSize) {
if (zoom >= config.maxzoom()) {
minPixelSizeAtMaxZoom = minPixelSize;
}
this.minPixelSize = ZoomFunction.maxZoom(zoom, minPixelSize);
return this;
}
/**
* Sets the minimum length of line features or square root of the minimum area of polygon features to emit at the
* maximum zoom-level of the map.
* <p>
* Since clients can "overzoom" in on features past the maximum zoom level, this is typically much smaller than min
* pixel size at lower zoom levels.
* <p>
* This overrides, but does not replace the default min pixel size or overrides set through other methods.
*/
public Feature setMinPixelSizeAtMaxZoom(double minPixelSize) {
this.minPixelSizeAtMaxZoom = minPixelSize;
return this;
}
/**
* Sets the minimum length of line features or square root of the minimum area of polygon features to emit at all
* zoom levels, including the maximum zoom-level of the map.
* <p>
* This replaces previous default values, but not overrides set with
* {@link #setMinPixelSizeOverrides(ZoomFunction)}.
*/
public Feature setMinPixelSizeAtAllZooms(int minPixelSize) {
this.minPixelSizeAtMaxZoom = minPixelSize;
return this.setMinPixelSize(minPixelSize);
}
/**
* Returns the simplification tolerance for lines and polygons in tile pixels at {@code zoom}.
*/
public double getPixelToleranceAtZoom(int zoom) {
return zoom == config.maxzoom() ? pixelToleranceAtMaxZoom :
ZoomFunction.applyAsDoubleOrElse(pixelTolerance, zoom, defaultPixelTolerance);
}
/**
* Sets the simplification tolerance for lines and polygons in tile pixels below the maximum zoom-level of the map.
* <p>
* Since clients can "overzoom" past the max zoom of the map, this is typically smaller than the default tolerance
* to provide more detail as you zoom in.
* <p>
* This does not replace any overrides that were set with {@link #setPixelToleranceOverrides(ZoomFunction)}.
*/
public Feature setPixelTolerance(double tolerance) {
this.defaultPixelTolerance = tolerance;
return this;
}
/**
* Sets the simplification tolerance for lines and polygons in tile pixels at the maximum zoom-level of the map.
* <p>
* This does not replace the default value at other zoom levels set through {@link #setPixelTolerance(double)} any
* zoom-specific overrides that were set with {@link #setPixelToleranceOverrides(ZoomFunction)}.
*/
public Feature setPixelToleranceAtMaxZoom(double tolerance) {
this.pixelToleranceAtMaxZoom = tolerance;
return this;
}
/**
* Sets the simplification tolerance for lines and polygons in tile pixels including at the maximum zoom-level of
* the map.
* <p>
* This does not replace the default value at other zoom levels set through {@link #setPixelTolerance(double)}.
*/
public Feature setPixelToleranceAtAllZooms(double tolerance) {
return setPixelToleranceAtMaxZoom(tolerance).setPixelTolerance(tolerance);
}
/**
* Sets zoom-specific overrides to the simplification tolerance for lines and polygons in tile pixels below the
* maximum zoom-level of the map.
* <p>
* At the maximum zoom level of the map, clients can "overzoom" in on features, so this leaves the tolerance at the
* max zoom level set to {@link PlanetilerConfig#simplifyToleranceAtMaxZoom()} unless you explicitly override it
* with {@link #setMinPixelSizeAtAllZooms(int)} or {@link #setMinPixelSizeAtMaxZoom(double)}.
* <p>
* If {@code levels} is {@code null} or returns {@code null}, the min pixel size will default to the default value.
*/
public Feature setPixelToleranceOverrides(ZoomFunction<Number> overrides) {
this.pixelTolerance = overrides;
return this;
}
/**
* Overrides the default simplification tolerance for lines and polygons in tile pixels at and below {@code zoom}
* with {@code minPixelSize}.
* <p>
* This replaces all previous zoom overrides that were set. To use multiple zoom-level thresholds, create a
* {@link ZoomFunction} explicitly and pass it to {@link #setPixelToleranceOverrides(ZoomFunction)}
*/
public Feature setPixelToleranceBelowZoom(int zoom, double tolerance) {
if (zoom == config.maxzoom()) {
pixelToleranceAtMaxZoom = tolerance;
}
return setPixelToleranceOverrides(ZoomFunction.maxZoom(zoom, tolerance));
}
public boolean hasLabelGrid() {
return labelGridPixelSize != null || labelGridLimit != null;
}
/**
* Returns the size in pixels of the grid used to group or limit output points.
*
* @throws AssertionError when assertions are enabled and the returned value is smaller than the buffer pixel size
*/
public double getPointLabelGridPixelSizeAtZoom(int zoom) {
double result = ZoomFunction.applyAsDoubleOrElse(labelGridPixelSize, zoom, DEFAULT_LABEL_GRID_SIZE);
// TODO is this enough? what about a grid square that ends just before the start of the tile
assert result <= getBufferPixelsAtZoom(
zoom) : "to avoid inconsistent rendering of the same point between adjacent tiles, buffer pixel size should be >= label grid size but in '%s' buffer pixel size=%f was greater than label grid size=%f"
.formatted(
getLayer(), getBufferPixelsAtZoom(zoom), result);
return result;
}
/**
* Returns the maximum number of lowest-sort-key points to include in the output vector tile in each square of a
* grid with size {@link #getPointLabelGridPixelSizeAtZoom(int)}.
*/
public int getPointLabelGridLimitAtZoom(int zoom) {
return ZoomFunction.applyAsIntOrElse(labelGridLimit, zoom, DEFAULT_LABEL_GRID_LIMIT);
}
/**
* Sets the size of a grid in tile pixels that will be used to compute a "location hash" of points rendered in each
* zoom-level for limiting the density of features in the output tile, or computing a "rank" key that clients can
* use to control label density.
* <p>
* If limit is set, features will be dropped automatically before encoding the vector tile, but "rank" must be added
* explicitly in {@link Profile#postProcessLayerFeatures(String, int, List)}.
* <p>
* Replaces any previous values set for label grid pixel size.
* <p>
* NOTE: the label grid is computed within each tile independently of its neighbors, so to ensure consistent limits
* and ranking of a point rendered in adjacent tiles, be sure to set the buffer pixel size to at least be larger
* than the label grid pixel size at each zoom level.
*
* @param labelGridSize a function that returns the size of the label grid to use at each zoom level. If function is
* or returns null for a zoom-level, no label grid will be computed.
* @see <a href="https://github.com/mapbox/postgis-vt-util/blob/master/src/LabelGrid.sql">LabelGrid postgis
* function</a>
*/
public Feature setPointLabelGridPixelSize(ZoomFunction<Number> labelGridSize) {
this.labelGridPixelSize = labelGridSize;
return this;
}
/**
* Sets the size of a grid in tile pixels that will be used to compute a "location hash" of points rendered in each
* zoom-level at and below {@code maxzoom}.
* <p>
* This is a thin wrapper around {@link #setPointLabelGridPixelSize(ZoomFunction)}. It replaces any previous value
* set for label grid size. To set multiple thresholds, use the other method directly.
*
* @see <a href="https://github.com/mapbox/postgis-vt-util/blob/master/src/LabelGrid.sql">LabelGrid postgis
* function</a>
*/
public Feature setPointLabelGridPixelSize(int maxzoom, double size) {
return setPointLabelGridPixelSize(ZoomFunction.maxZoom(maxzoom, size));
}
/**
* Sets the maximum number of points with the lowest sort-key to include with the same label grid hash in a tile.
* <p>
* Replaces any previous values set for label grid limit.
*
* @param labelGridLimit a function that returns the size of the label grid to use at each zoom level. If function
* is or returns null for a zoom-level, no label grid will be computed.
* @see <a href="https://github.com/mapbox/postgis-vt-util/blob/master/src/LabelGrid.sql">LabelGrid postgis
* function</a>
*/
public Feature setPointLabelGridLimit(ZoomFunction<Number> labelGridLimit) {
this.labelGridLimit = labelGridLimit;
return this;
}
/**
* Limits the density of points on an output tile at and below {@code maxzoom} by only emitting the {@code limit}
* features with lowest sort-key in each square of a {@code size x size} pixel grid.
* <p>
* This is a thin wrapper around {@link #setPointLabelGridPixelSize(ZoomFunction)} and
* {@link #setPointLabelGridLimit(ZoomFunction)}. It replaces any previous value set for label grid size or limit.
* To set multiple thresholds, use the other methods directly.
* <p>
* NOTE: the label grid is computed within each tile independently of its neighbors, so to ensure consistent limits
* and ranking of a point rendered in adjacent tiles, be sure to set the buffer pixel size to at least be larger
* than the label grid pixel size at each zoom level.
*
* @param maxzoom the zoom-level at and below which we should limit point density
* @param size the label grid size to use when computing hashes
* @param limit the number of lowest-sort-key points to include in each square of the grid
* @see <a href="https://github.com/mapbox/postgis-vt-util/blob/master/src/LabelGrid.sql">LabelGrid postgis
* function</a>
*/
public Feature setPointLabelGridSizeAndLimit(int maxzoom, double size, int limit) {
return setPointLabelGridPixelSize(ZoomFunction.maxZoom(maxzoom, size))
.setPointLabelGridLimit(ZoomFunction.maxZoom(maxzoom, limit));
}
// could be expensive, so cache results
private Map<String, Object> computeAttrsAtZoom(int zoom) {
Map<String, Object> result = new TreeMap<>();
for (var entry : attrs.entrySet()) {
Object value = entry.getValue();
if (value instanceof ZoomFunction<?> fn) {
value = fn.apply(zoom);
}
if (value != null && !"".equals(value)) {
result.put(entry.getKey(), value);
}
}
return result;
}
/** Returns the attribute to put on all output vector tile features at a zoom level. */
public Map<String, Object> getAttrsAtZoom(int zoom) {
if (!attrsChangeByZoom) {
return attrs;
}
if (attrCache == null) {
attrCache = CacheByZoom.create(config, this::computeAttrsAtZoom);
}
return attrCache.get(zoom);
}
/** Copies the value for {@code key} attribute from source feature to the output feature. */
public Feature inheritAttrFromSource(String key) {
return setAttr(key, source.getTag(key));
}
/**
* Sets an attribute on the output feature to either a string, number, boolean, or instance of {@link ZoomFunction}
* to change the value for {@code key} by zoom-level.
*/
public Feature setAttr(String key, Object value) {
if (value instanceof ZoomFunction) {
attrsChangeByZoom = true;
}
if (value != null) {
attrs.put(key, value);
}
return this;
}
/**
* Sets the value for {@code key} attribute at or above {@code minzoom}. Below {@code minzoom} it will be ignored.
* <p>
* Replaces all previous value that has been for {@code key} at any zoom level. To have a value that changes at
* multiple zoom level thresholds, call {@link #setAttr(String, Object)} with a manually-constructed
* {@link ZoomFunction} value.
*/
public Feature setAttrWithMinzoom(String key, Object value, int minzoom) {
return setAttr(key, ZoomFunction.minZoom(minzoom, value));
}
/**
* Inserts all key/value pairs in {@code attrs} into the set of attribute to emit on the output feature at or above
* {@code minzoom}.
* <p>
* Replaces values that have already been set.
*/
public Feature putAttrsWithMinzoom(Map<String, Object> attrs, int minzoom) {
for (var entry : attrs.entrySet()) {
setAttrWithMinzoom(entry.getKey(), entry.getValue(), minzoom);
}
return this;
}
/**
* Inserts all key/value pairs in {@code attrs} into the set of attribute to emit on the output feature.
* <p>
* Does not touch attributes that have already been set.
* <p>
* Values in {@code attrs} can either be the raw value to set, or an instance of {@link ZoomFunction} to change the
* value for that attribute by zoom level.
*/
public Feature putAttrs(Map<String, Object> attrs) {
for (Object value : attrs.values()) {
if (value instanceof ZoomFunction) {
attrsChangeByZoom = true;
break;
}
}
this.attrs.putAll(attrs);
return this;
}
/**
* Sets a special attribute key that the renderer will use to store the number of points in the simplified geometry
* before slicing it into tiles.
*/
public Feature setNumPointsAttr(String numPointsAttr) {
this.numPointsAttr = numPointsAttr;
return this;
}
/**
* Returns the attribute key that the renderer should use to store the number of points in the simplified geometry
* before slicing it into tiles.
*/
public String getNumPointsAttr() {
return numPointsAttr;
}
@Override
public String toString() {
return "Feature{" +
"layer='" + layer + '\'' +
", geom=" + geom.getGeometryType() +
", attrs=" + attrs +
'}';
}
}
}