Skip to content

Commit c2ec15f

Browse files
Rohten-devDominik Winkler
andauthored
add CLI option to adjust the clip buffer size (#564)
Co-authored-by: Dominik Winkler <dominik.winkler@materna.group>
1 parent 8b566b9 commit c2ec15f

3 files changed

Lines changed: 69 additions & 16 deletions

File tree

tiles/src/main/java/com/protomaps/basemap/Basemap.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ private static void printHelp() {
188188
Valid values: boundaries, buildings, landuse, landcover,
189189
places, pois, roads, transit, water, earth
190190
--clip=<path> GeoJSON file path to clip tileset (optional)
191+
--clip-buffer=<n> Relative buffer around clip polygon (default: 4.0/256.0)
192+
Use 0 for exact boundary, e.g. --clip-buffer=0
191193
192194
Common Planetiler Options:
193195
--output=<path> Output file path and format (e.g., output.pmtiles)
@@ -243,11 +245,17 @@ static void run(Arguments args) throws IOException {
243245
fontRegistry.setZipFilePath(pgfEncodingZip.toString());
244246

245247
Clip clip = null;
248+
double clipBuffer = args.getDouble("clip_buffer",
249+
"Relative buffer around clip polygon. 0 for exact boundary.", Clip.DEFAULT_BUFFER);
250+
if (clipBuffer < 0) {
251+
LOGGER.error("Error: --clip-buffer must be >= 0, but was {}", clipBuffer);
252+
System.exit(1);
253+
}
246254
var clipArg = args.getString("clip", "File path to GeoJSON Polygon or MultiPolygon geometry to clip tileset.", "");
247255
if (!clipArg.isEmpty()) {
248256
clip =
249257
Clip.fromGeoJSONFile(args.getStats(), planetiler.config().minzoom(), planetiler.config().maxzoom(), true,
250-
Paths.get(clipArg));
258+
clipBuffer, Paths.get(clipArg));
251259
}
252260

253261
List<String> availableLayers = List.of(

tiles/src/main/java/com/protomaps/basemap/postprocess/Clip.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,20 @@ public class Clip implements ForwardingProfile.TilePostProcessor {
2424
private final Map<Integer, Map<TileCoord, List<List<CoordinateSequence>>>> boundaryTilesByZoom;
2525
private final Map<Integer, TiledGeometry.CoveredTiles> coveredTilesByZoom;
2626
private final Stats stats;
27+
private final double boundaryBuffer;
2728

28-
static final double DEFAULT_BUFFER = 4.0 / 256.0;
29+
public static final double DEFAULT_BUFFER = 4.0;
2930

3031
// A TilePostProcessor that clips all layers to a given geometry.
3132
// the geometry must be in world coordinates ( world from 0 to 1 )
32-
public Clip(Stats stats, int minzoom, int maxzoom, boolean doBuffer, Geometry input) {
33+
public Clip(Stats stats, int minzoom, int maxzoom, boolean doBuffer, double clipBuffer, Geometry input) {
3334
this.stats = stats;
35+
this.boundaryBuffer = clipBuffer / 256.0;
36+
3437
double bufferAmount = 0;
3538
if (doBuffer) {
3639
var envelope = input.getEnvelope().getEnvelopeInternal();
37-
bufferAmount = Math.max(envelope.getWidth(), envelope.getHeight()) * DEFAULT_BUFFER;
40+
bufferAmount = Math.max(envelope.getWidth(), envelope.getHeight()) * boundaryBuffer;
3841
}
3942
var clipGeometry = input.buffer(bufferAmount);
4043
boundaryTilesByZoom = new HashMap<>();
@@ -45,21 +48,22 @@ public Clip(Stats stats, int minzoom, int maxzoom, boolean doBuffer, Geometry in
4548
double scale = 1 << i;
4649
Geometry scaled = AffineTransformation.scaleInstance(scale, scale).transform(clipGeometry);
4750
this.boundaryTilesByZoom.put(i,
48-
sliceIntoTiles(scaled, 0, DEFAULT_BUFFER, i, extents.getForZoom(i)).getTileData());
51+
sliceIntoTiles(scaled, 0, boundaryBuffer, i, extents.getForZoom(i)).getTileData());
4952
this.coveredTilesByZoom.put(i, getCoveredTiles(scaled, i, extents.getForZoom(i)));
5053
}
5154
} catch (GeometryException e) {
5255
throw new Planetiler.PlanetilerException("Error clipping", e);
5356
}
5457
}
5558

56-
public static Clip fromGeoJSONFile(Stats stats, int minzoom, int maxzoom, boolean doBuffer, Path path) {
59+
public static Clip fromGeoJSONFile(Stats stats, int minzoom, int maxzoom, boolean doBuffer, double clipBuffer,
60+
Path path) {
5761
var g = GeoJson.from(path);
5862
if (g.count() == 0) {
5963
throw new FileFormatException("Empty clipping geometry");
6064
}
6165
var feature = g.iterator().next();
62-
return new Clip(stats, minzoom, maxzoom, doBuffer, latLonToWorldCoords(feature.geometry()));
66+
return new Clip(stats, minzoom, maxzoom, doBuffer, clipBuffer, latLonToWorldCoords(feature.geometry()));
6367
}
6468

6569
// Copied from elsewhere in planetiler

tiles/src/test/java/com/protomaps/basemap/postprocess/ClipTest.java

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class ClipTest {
2222
void testLoadGeoJSON() {
2323
Path cwd = Path.of("").toAbsolutePath();
2424
Path pathFromRoot = Path.of("tiles", "src", "test", "resources", "clip.geojson");
25-
var clip = Clip.fromGeoJSONFile(stats, 0, 0, false, cwd.resolveSibling(pathFromRoot));
25+
var clip = Clip.fromGeoJSONFile(stats, 0, 0, false, Clip.DEFAULT_BUFFER, cwd.resolveSibling(pathFromRoot));
2626
assertNotNull(clip);
2727
}
2828

@@ -32,7 +32,7 @@ void testLoadNonJSON() {
3232
Path pathFromRoot = Path.of("tiles", "src", "test", "resources", "empty.geojson");
3333
Path path = cwd.resolveSibling(pathFromRoot);
3434
assertThrows(FileFormatException.class, () -> {
35-
Clip.fromGeoJSONFile(stats, 0, 0, false, path);
35+
Clip.fromGeoJSONFile(stats, 0, 0, false, Clip.DEFAULT_BUFFER, path);
3636
});
3737
}
3838

@@ -46,7 +46,8 @@ void testClipLine() throws GeometryException {
4646
));
4747

4848
// a rectangle that is 50% of the earths width, centered at null island.
49-
var n = new Clip(stats, 0, 0, false, newPolygon(0.25, 0.25, 0.75, 0.25, 0.75, 0.75, 0.25, 0.75, 0.25, 0.25));
49+
var n = new Clip(stats, 0, 0, false, Clip.DEFAULT_BUFFER,
50+
newPolygon(0.25, 0.25, 0.75, 0.25, 0.75, 0.75, 0.25, 0.75, 0.25, 0.25));
5051
var clipped = n.postProcessTile(TileCoord.ofXYZ(0, 0, 0), Map.of("layer", unclipped));
5152

5253
assertEquals(1, clipped.size());
@@ -64,7 +65,8 @@ void testClipLineMulti() throws GeometryException {
6465
));
6566

6667
// a rectangle that is 50% of the earths width, centered at null island.
67-
var n = new Clip(stats, 0, 0, false, newPolygon(0.25, 0.25, 0.75, 0.25, 0.75, 0.75, 0.25, 0.75, 0.25, 0.25));
68+
var n = new Clip(stats, 0, 0, false, Clip.DEFAULT_BUFFER,
69+
newPolygon(0.25, 0.25, 0.75, 0.25, 0.75, 0.75, 0.25, 0.75, 0.25, 0.25));
6870
var clipped = n.postProcessTile(TileCoord.ofXYZ(0, 0, 0), Map.of("layer", unclipped));
6971

7072
assertEquals(1, clipped.size());
@@ -82,7 +84,8 @@ void testClipPolygon() throws GeometryException {
8284
));
8385

8486
// a rectangle that is 50% of the earths width, centered at null island.
85-
var n = new Clip(stats, 0, 0, false, newPolygon(0.25, 0.25, 0.75, 0.25, 0.75, 0.75, 0.25, 0.75, 0.25, 0.25));
87+
var n = new Clip(stats, 0, 0, false, Clip.DEFAULT_BUFFER,
88+
newPolygon(0.25, 0.25, 0.75, 0.25, 0.75, 0.75, 0.25, 0.75, 0.25, 0.25));
8689
var clipped = n.postProcessTile(TileCoord.ofXYZ(0, 0, 0), Map.of("layer", unclipped));
8790

8891
assertEquals(1, clipped.size());
@@ -99,7 +102,8 @@ void testClipBelowMinZoom() throws GeometryException {
99102
Map.of("foo", "bar")
100103
));
101104

102-
var n = new Clip(stats, 1, 1, false, newPolygon(0.25, 0.25, 0.75, 0.25, 0.75, 0.75, 0.25, 0.75, 0.25, 0.25));
105+
var n = new Clip(stats, 1, 1, false, Clip.DEFAULT_BUFFER,
106+
newPolygon(0.25, 0.25, 0.75, 0.25, 0.75, 0.75, 0.25, 0.75, 0.25, 0.25));
103107
var clipped = n.postProcessTile(TileCoord.ofXYZ(0, 0, 0), Map.of("layer", unclipped));
104108
assertEquals(0, clipped.size());
105109
}
@@ -112,7 +116,8 @@ void testClipWhollyOutside() throws GeometryException {
112116
Map.of("foo", "bar")
113117
));
114118

115-
var n = new Clip(stats, 0, 0, false, newPolygon(0.25, 0.25, 0.75, 0.25, 0.75, 0.75, 0.25, 0.75, 0.25, 0.25));
119+
var n = new Clip(stats, 0, 0, false, Clip.DEFAULT_BUFFER,
120+
newPolygon(0.25, 0.25, 0.75, 0.25, 0.75, 0.75, 0.25, 0.75, 0.25, 0.25));
116121
var clipped = n.postProcessTile(TileCoord.ofXYZ(0, 0, 0), Map.of("layer", unclipped));
117122
assertEquals(0, clipped.size());
118123
}
@@ -125,7 +130,8 @@ void testClipInInterior() throws GeometryException {
125130
Map.of("foo", "bar")
126131
));
127132

128-
var n = new Clip(stats, 0, 3, false, newPolygon(0.25, 0.25, 0.75, 0.25, 0.75, 0.75, 0.25, 0.75, 0.25, 0.25));
133+
var n = new Clip(stats, 0, 3, false, Clip.DEFAULT_BUFFER,
134+
newPolygon(0.25, 0.25, 0.75, 0.25, 0.75, 0.75, 0.25, 0.75, 0.25, 0.25));
129135
var clipped = n.postProcessTile(TileCoord.ofXYZ(3, 3, 3), Map.of("layer", unclipped));
130136
assertEquals(1, clipped.size());
131137
assertEquals(1, clipped.get("layer").size());
@@ -140,11 +146,46 @@ void testClipLineBuffer() throws GeometryException {
140146
));
141147

142148
// a rectangle that is 50% of the earths width, centered at null island.
143-
var n = new Clip(stats, 0, 0, true, newPolygon(0.25, 0.25, 0.75, 0.25, 0.75, 0.75, 0.25, 0.75, 0.25, 0.25));
149+
var n = new Clip(stats, 0, 0, true, Clip.DEFAULT_BUFFER,
150+
newPolygon(0.25, 0.25, 0.75, 0.25, 0.75, 0.75, 0.25, 0.75, 0.25, 0.25));
144151
var clipped = n.postProcessTile(TileCoord.ofXYZ(0, 0, 0), Map.of("layer", unclipped));
145152

146153
assertEquals(1, clipped.size());
147154
assertEquals(1, clipped.get("layer").size());
148155
assertEquals(newLineString(62, 128, 194, 128), clipped.get("layer").getFirst().geometry().decode());
149156
}
157+
158+
@Test
159+
void testClipLineBufferZero() throws GeometryException {
160+
List<VectorTile.Feature> unclipped = new ArrayList<>();
161+
unclipped.add(new VectorTile.Feature("layer", 1,
162+
VectorTile.encodeGeometry(newLineString(0, 128, 256, 128)),
163+
Map.of("foo", "bar")
164+
));
165+
166+
var n = new Clip(stats, 0, 0, true, 0,
167+
newPolygon(0.25, 0.25, 0.75, 0.25, 0.75, 0.75, 0.25, 0.75, 0.25, 0.25));
168+
var clipped = n.postProcessTile(TileCoord.ofXYZ(0, 0, 0), Map.of("layer", unclipped));
169+
170+
assertEquals(1, clipped.size());
171+
assertEquals(1, clipped.get("layer").size());
172+
assertEquals(newLineString(64, 128, 192, 128), clipped.get("layer").getFirst().geometry().decode());
173+
}
174+
175+
@Test
176+
void testClipLineBufferLargerThanDefault() throws GeometryException {
177+
List<VectorTile.Feature> unclipped = new ArrayList<>();
178+
unclipped.add(new VectorTile.Feature("layer", 1,
179+
VectorTile.encodeGeometry(newLineString(0, 128, 256, 128)),
180+
Map.of("foo", "bar")
181+
));
182+
183+
var n = new Clip(stats, 0, 0, true, 8.0,
184+
newPolygon(0.25, 0.25, 0.75, 0.25, 0.75, 0.75, 0.25, 0.75, 0.25, 0.25));
185+
var clipped = n.postProcessTile(TileCoord.ofXYZ(0, 0, 0), Map.of("layer", unclipped));
186+
187+
assertEquals(1, clipped.size());
188+
assertEquals(1, clipped.get("layer").size());
189+
assertEquals(newLineString(60, 128, 196, 128), clipped.get("layer").getFirst().geometry().decode());
190+
}
150191
}

0 commit comments

Comments
 (0)