package com.onthegomap.planetiler;
import com.carrotsearch.hppc.IntArrayList;
import com.carrotsearch.hppc.IntObjectMap;
import com.carrotsearch.hppc.IntStack;
import com.onthegomap.planetiler.collection.Hppc;
import com.onthegomap.planetiler.geo.DouglasPeuckerSimplifier;
import com.onthegomap.planetiler.geo.GeoUtils;
import com.onthegomap.planetiler.geo.GeometryException;
import com.onthegomap.planetiler.geo.GeometryType;
import com.onthegomap.planetiler.geo.MutableCoordinateSequence;
import com.onthegomap.planetiler.stats.DefaultStats;
import com.onthegomap.planetiler.stats.Stats;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.locationtech.jts.algorithm.Area;
import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.Polygonal;
import org.locationtech.jts.index.strtree.STRtree;
import org.locationtech.jts.operation.buffer.BufferOp;
import org.locationtech.jts.operation.buffer.BufferParameters;
import org.locationtech.jts.operation.linemerge.LineMerger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A collection of utilities for merging features with the same attributes in a rendered tile from
* {@link Profile#postProcessLayerFeatures(String, int, List)} immediately before a tile is written to the output
* archive.
*
* Unlike postgis-based solutions that have a full view of all features after they are loaded into the database, the
* planetiler engine only sees a single input feature at a time while processing source features, then only has
* visibility into multiple features when they are grouped into a tile immediately before emitting. This ends up being
* sufficient for most real-world use-cases but to do anything more that requires a view of multiple features
* not within the same tile, {@link Profile} implementations must store input features manually.
*/
public class FeatureMerge {
private static final Logger LOGGER = LoggerFactory.getLogger(FeatureMerge.class);
private static final BufferParameters bufferOps = new BufferParameters();
// this is slightly faster than Comparator.comparingInt
private static final Comparator> BY_HILBERT_INDEX =
(o1, o2) -> Integer.compare(o1.hilbert, o2.hilbert);
static {
bufferOps.setJoinStyle(BufferParameters.JOIN_MITRE);
}
/** Don't instantiate */
private FeatureMerge() {}
/**
* Combines linestrings with the same set of attributes into a multilinestring where segments with touching endpoints
* are merged by {@link LineMerger}, removing any linestrings under {@code minLength}.
*
* Ignores any non-linestrings and passes them through to the output unaltered.
*
* Orders grouped output multilinestring by the index of the first element in that group from the input list.
*
* @param features all features in a layer
* @param minLength minimum tile pixel length of features to emit, or 0 to emit all merged linestrings
* @param tolerance after merging, simplify linestrings using this pixel tolerance, or -1 to skip simplification step
* @param buffer number of pixels outside the visible tile area to include detail for, or -1 to skip clipping step
* @param resimplify True if linestrings should be simplified even if they don't get merged with another
* @return a new list containing all unaltered features in their original order, then each of the merged groups
* ordered by the index of the first element in that group from the input list.
*/
public static List mergeLineStrings(List features,
double minLength, double tolerance, double buffer, boolean resimplify) {
return mergeLineStrings(features, attrs -> minLength, tolerance, buffer, resimplify);
}
/**
* Merges linestrings with the same attributes as {@link #mergeLineStrings(List, double, double, double, boolean)}
* except sets {@code resimplify=false} by default.
*/
public static List mergeLineStrings(List features,
double minLength, double tolerance, double buffer) {
return mergeLineStrings(features, minLength, tolerance, buffer, false);
}
/** Merges points with the same attributes into multipoints. */
public static List mergeMultiPoint(List features) {
return mergeGeometries(features, GeometryType.POINT);
}
/**
* Merges polygons with the same attributes into multipolygons.
*
* NOTE: This does not attempt to combine overlapping geometries, see {@link #mergeOverlappingPolygons(List, double)}
* or {@link #mergeNearbyPolygons(List, double, double, double, double)} for that.
*/
public static List mergeMultiPolygon(List features) {
return mergeGeometries(features, GeometryType.POLYGON);
}
/**
* Merges linestrings with the same attributes into multilinestrings.
*
* NOTE: This does not attempt to connect linestrings that intersect at endpoints, see
* {@link #mergeLineStrings(List, double, double, double, boolean)} for that. Also, this removes extra detail that was
* preserved to improve connected-linestring merging, so you should only use one or the other.
*/
public static List mergeMultiLineString(List features) {
return mergeGeometries(features, GeometryType.LINE);
}
private static List mergeGeometries(
List features,
GeometryType geometryType
) {
List result = new ArrayList<>(features.size());
var groupedByAttrs = groupByAttrs(features, result, geometryType);
for (List groupedFeatures : groupedByAttrs) {
VectorTile.Feature feature1 = groupedFeatures.getFirst();
if (groupedFeatures.size() == 1) {
result.add(feature1);
} else {
VectorTile.VectorGeometryMerger combined = VectorTile.newMerger(geometryType);
groupedFeatures.stream()
.map(f -> new WithIndex<>(f, f.geometry().hilbertIndex()))
.sorted(BY_HILBERT_INDEX)
.map(d -> d.feature.geometry())
.forEachOrdered(combined);
result.add(feature1.copyWithNewGeometry(combined.finish()));
}
}
return result;
}
/**
* Merges linestrings with the same attributes as {@link #mergeLineStrings(List, Function, double, double, boolean)}
* except sets {@code resimplify=false} by default.
*/
public static List mergeLineStrings(List features,
Function