-
-
Notifications
You must be signed in to change notification settings - Fork 56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Clip tileset to a polygonal boundary #351
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package com.protomaps.basemap; | ||
|
||
import com.fasterxml.jackson.databind.node.ArrayNode; | ||
import com.onthegomap.planetiler.geo.GeoUtils; | ||
import org.locationtech.jts.geom.Coordinate; | ||
import com.fasterxml.jackson.databind.JsonNode; | ||
import org.locationtech.jts.geom.Geometry; | ||
import org.locationtech.jts.geom.LinearRing; | ||
import org.locationtech.jts.geom.Polygon; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
public class GeoJSON { | ||
private static Coordinate[] parseCoordinates(ArrayNode coordinateArray) { | ||
Coordinate[] coordinates = new Coordinate[coordinateArray.size()]; | ||
for (int i = 0; i < coordinateArray.size(); i++) { | ||
ArrayNode coordinate = (ArrayNode) coordinateArray.get(i); | ||
double x = coordinate.get(0).asDouble(); | ||
double y = coordinate.get(1).asDouble(); | ||
coordinates[i] = new Coordinate(x, y); | ||
} | ||
return coordinates; | ||
} | ||
|
||
private static Polygon coordsToPolygon(JsonNode coords) { | ||
ArrayNode outerRingNode = (ArrayNode) coords.get(0); | ||
Coordinate[] outerRingCoordinates = parseCoordinates(outerRingNode); | ||
LinearRing outerRing = GeoUtils.JTS_FACTORY.createLinearRing(outerRingCoordinates); | ||
|
||
LinearRing[] innerRings = new LinearRing[coords.size() - 1]; | ||
for (int j = 1; j < coords.size(); j++) { | ||
ArrayNode innerRingNode = (ArrayNode) coords.get(j); | ||
Coordinate[] innerRingCoordinates = parseCoordinates(innerRingNode); | ||
innerRings[j - 1] = GeoUtils.JTS_FACTORY.createLinearRing(innerRingCoordinates); | ||
} | ||
return GeoUtils.JTS_FACTORY.createPolygon(outerRing, innerRings); | ||
} | ||
|
||
// return a Polygon or MultiPolygon from a GeoJSON geometry object. | ||
public static Geometry parseGeometry(JsonNode jsonGeometry) { | ||
var coords = jsonGeometry.get("coordinates"); | ||
if (jsonGeometry.get("type").asText().equals("Polygon")) { | ||
return coordsToPolygon(coords); | ||
} else if (jsonGeometry.get("type").asText().equals("MultiPolygon")) { | ||
List<Polygon> polygons = new ArrayList<>(); | ||
for (var polygonCoords : coords) { | ||
polygons.add(coordsToPolygon(polygonCoords)); | ||
} | ||
return GeoUtils.createMultiPolygon(polygons); | ||
} else { | ||
throw new IllegalArgumentException(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
package com.protomaps.basemap.postprocess; | ||
|
||
import static com.onthegomap.planetiler.geo.GeoUtils.WORLD_BOUNDS; | ||
import static com.onthegomap.planetiler.geo.GeoUtils.latLonToWorldCoords; | ||
import static com.onthegomap.planetiler.render.TiledGeometry.getCoveredTiles; | ||
import static com.onthegomap.planetiler.render.TiledGeometry.sliceIntoTiles; | ||
|
||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.onthegomap.planetiler.ForwardingProfile; | ||
import com.onthegomap.planetiler.VectorTile; | ||
import com.onthegomap.planetiler.geo.*; | ||
import com.onthegomap.planetiler.render.TiledGeometry; | ||
import java.io.IOException; | ||
import java.nio.file.Files; | ||
import java.nio.file.Paths; | ||
import java.util.*; | ||
|
||
import com.onthegomap.planetiler.stats.Stats; | ||
import com.protomaps.basemap.GeoJSON; | ||
import org.locationtech.jts.geom.*; | ||
import org.locationtech.jts.geom.util.AffineTransformation; | ||
import org.locationtech.jts.operation.overlayng.OverlayNG; | ||
import org.locationtech.jts.operation.overlayng.OverlayNGRobust; | ||
|
||
public class Clip implements ForwardingProfile.TilePostProcessor { | ||
private final Map<Integer, Map<TileCoord, List<List<CoordinateSequence>>>> tiledGeometries; | ||
private final Map<Integer, TiledGeometry.CoveredTiles> coverings; | ||
private final Stats stats; | ||
|
||
public Clip(Stats stats, Geometry input) { | ||
this.stats = stats; | ||
var clipGeometry = latLonToWorldCoords(input).buffer(0.00001); | ||
tiledGeometries = new HashMap<>(); | ||
coverings = new HashMap<>(); | ||
try { | ||
for (var i = 0; i <= 15; i++) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to get min/max from the actual program somewhere. |
||
var extents = TileExtents.computeFromWorldBounds(i, WORLD_BOUNDS); | ||
double scale = 1 << i; | ||
Geometry scaled = AffineTransformation.scaleInstance(scale, scale).transform(clipGeometry); | ||
// var simplified = DouglasPeuckerSimplifier.simplify(scaled, 0.25/256); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to simplify, or rely on the last snapping step? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would be surprised if we had to. |
||
this.tiledGeometries.put(i, sliceIntoTiles(scaled, 0, 0.015625, i, extents.getForZoom(i)).getTileData()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We hardcode the buffer size here. We need a way to determine the actual buffer size taking into account any custom buffer size determination function. |
||
this.coverings.put(i, getCoveredTiles(scaled, i, extents.getForZoom(i))); | ||
} | ||
} catch (GeometryException e) { | ||
throw new RuntimeException("Error clipping"); | ||
} | ||
} | ||
|
||
public static Clip fromGeoJSONFile(Stats stats, String filename) { | ||
try { | ||
return fromGeoJSON(stats, Files.readAllBytes(Paths.get(filename))); | ||
} catch (IOException e) { | ||
throw new IllegalArgumentException("Could not open clip file"); | ||
} | ||
} | ||
|
||
public static Clip fromGeoJSON(Stats stats, byte[] bytes) { | ||
try { | ||
ObjectMapper mapper = new ObjectMapper(); | ||
JsonNode geoJson = mapper.readTree(bytes); | ||
return new Clip(stats, GeoJSON.parseGeometry(geoJson)); | ||
} catch (IOException e) { | ||
throw new IllegalArgumentException("Clip GeoJSON is invalid"); | ||
} | ||
} | ||
|
||
// Copied from elsewhere in planetiler | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we could make these public in planetiler-core? |
||
private static Polygon reassemblePolygon(List<CoordinateSequence> group) throws GeometryException { | ||
try { | ||
LinearRing first = GeoUtils.JTS_FACTORY.createLinearRing(group.getFirst()); | ||
LinearRing[] rest = new LinearRing[group.size() - 1]; | ||
for (int j = 1; j < group.size(); j++) { | ||
CoordinateSequence seq = group.get(j); | ||
CoordinateSequences.reverse(seq); | ||
rest[j - 1] = GeoUtils.JTS_FACTORY.createLinearRing(seq); | ||
} | ||
return GeoUtils.JTS_FACTORY.createPolygon(first, rest); | ||
} catch (IllegalArgumentException e) { | ||
throw new GeometryException("reassemble_polygon_failed", "Could not build polygon", e); | ||
} | ||
} | ||
|
||
// Copied from elsewhere in Planetiler | ||
static Geometry reassemblePolygons(List<List<CoordinateSequence>> groups) throws GeometryException { | ||
int numGeoms = groups.size(); | ||
if (numGeoms == 1) { | ||
return reassemblePolygon(groups.getFirst()); | ||
} else { | ||
Polygon[] polygons = new Polygon[numGeoms]; | ||
for (int i = 0; i < numGeoms; i++) { | ||
polygons[i] = reassemblePolygon(groups.get(i)); | ||
} | ||
return GeoUtils.JTS_FACTORY.createMultiPolygon(polygons); | ||
} | ||
} | ||
|
||
@Override | ||
public Map<String, List<VectorTile.Feature>> postProcessTile(TileCoord tileCoord, | ||
Map<String, List<VectorTile.Feature>> map) throws GeometryException { | ||
if (this.coverings.containsKey(tileCoord.z()) && | ||
this.coverings.get(tileCoord.z()).test(tileCoord.x(), tileCoord.y())) { | ||
if (this.tiledGeometries.containsKey(tileCoord.z()) && this.tiledGeometries.get(tileCoord.z()).containsKey(tileCoord)) { | ||
List<List<CoordinateSequence>> coords = tiledGeometries.get(tileCoord.z()).get(tileCoord); | ||
var clipGeometry = reassemblePolygons(coords); | ||
var clipGeometry2 = GeoUtils.fixPolygon(clipGeometry); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
clipGeometry2.reverse(); | ||
Map<String, List<VectorTile.Feature>> output = new HashMap<>(); | ||
|
||
for (Map.Entry<String, List<VectorTile.Feature>> layer : map.entrySet()) { | ||
List<VectorTile.Feature> clippedFeatures = new ArrayList<>(); | ||
for (var feature : layer.getValue()) { | ||
try { | ||
var newGeom = OverlayNGRobust.overlay(feature.geometry().decode(), clipGeometry2, OverlayNG.INTERSECTION); | ||
if (!newGeom.isEmpty() && newGeom.getNumGeometries() > 0) { | ||
if (newGeom instanceof Polygonal) { | ||
newGeom = GeoUtils.snapAndFixPolygon(newGeom, stats, "clip"); | ||
newGeom = newGeom.reverse(); | ||
if (!newGeom.isEmpty() && newGeom.getNumGeometries() > 0) { | ||
if (newGeom instanceof GeometryCollection) { | ||
for (int i = 0; i < newGeom.getNumGeometries(); i++) { | ||
// geometrycollection | ||
clippedFeatures.add(feature.copyWithNewGeometry(newGeom.getGeometryN(i))); | ||
} | ||
} else { | ||
// a multipolygon/polygon | ||
clippedFeatures.add(feature.copyWithNewGeometry(newGeom)); | ||
} | ||
} | ||
} else { | ||
if (!newGeom.isEmpty() && newGeom.getNumGeometries() > 0) { | ||
if (newGeom instanceof GeometryCollection) { | ||
for (int i = 0; i < newGeom.getNumGeometries(); i++) { | ||
clippedFeatures.add(feature.copyWithNewGeometry(newGeom.getGeometryN(i))); | ||
} | ||
} else { | ||
clippedFeatures.add(feature.copyWithNewGeometry(newGeom)); | ||
} | ||
} | ||
} | ||
} | ||
} catch (GeometryException e) { | ||
System.err.println("Could not clip geometry"); | ||
} | ||
} | ||
|
||
output.put(layer.getKey(), clippedFeatures); | ||
} | ||
return output; | ||
} | ||
return map; | ||
} | ||
return Map.of(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package com.protomaps.basemap; | ||
|
||
public class GeoJSONTest { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package com.protomaps.basemap.postprocess; | ||
|
||
public class ClipTest { | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this should be configurable buffer size via arguments?