Skip to content
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

Ensure GeometryUtil.toJts() returns valid geometry #4668

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions src/main/java/net/rptools/lib/GeometryUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,29 @@
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.locationtech.jts.awt.ShapeReader;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.PrecisionModel;
import org.locationtech.jts.noding.NodableSegmentString;
import org.locationtech.jts.noding.NodedSegmentString;
import org.locationtech.jts.noding.SegmentString;
import org.locationtech.jts.noding.snapround.SnapRoundingNoder;
import org.locationtech.jts.operation.polygonize.Polygonizer;
import org.locationtech.jts.operation.valid.IsValidOp;

public class GeometryUtil {
private static final Logger log = LogManager.getLogger(GeometryUtil.class);
private static final PrecisionModel precisionModel = new PrecisionModel(1_000_000.0);

private static final PrecisionModel precisionModel = new PrecisionModel(100_000.0);

private static final GeometryFactory geometryFactory = new GeometryFactory(precisionModel);

public static double getAngle(Point2D origin, Point2D target) {
Expand Down Expand Up @@ -117,11 +121,10 @@ public static GeometryFactory getGeometryFactory() {
}

private static Polygonizer toPolygonizer(Area area) {
final var pathIterator = area.getPathIterator(null);
final var polygonizer = new Polygonizer(true);
final var coords = (List<Coordinate[]>) ShapeReader.toCoordinates(pathIterator);
final var pathIterator = area.getPathIterator(null, 1. / precisionModel.getScale());

// Make sure the geometry is noded and precise before polygonizing.
final var coords = (List<Coordinate[]>) ShapeReader.toCoordinates(pathIterator);
final var strings = new ArrayList<NodableSegmentString>(coords.size());
for (var string : coords) {
strings.add(new NodedSegmentString(string, null));
Expand All @@ -132,6 +135,7 @@ private static Polygonizer toPolygonizer(Area area) {
final Collection<? extends SegmentString> nodedStrings = noder.getNodedSubstrings();

// Now build the polygons from our corrected geometry.
final var polygonizer = new Polygonizer(true);
for (var string : nodedStrings) {
final var lineString = geometryFactory.createLineString(string.getCoordinates());
polygonizer.add(lineString);
Expand All @@ -151,11 +155,20 @@ private static Polygonizer toPolygonizer(Area area) {
return polygonizer;
}

public static Geometry toJts(Area area) {
return toPolygonizer(area).getGeometry();
public static MultiPolygon toJts(Area area) {
final var polygons = toJtsPolygons(area);
final var geometry = geometryFactory.createMultiPolygon(polygons.toArray(Polygon[]::new));
assert geometry.isValid()
: "Returned geometry must be valid, but found this error: "
+ new IsValidOp(geometry).getValidationError();
return geometry;
}

public static Collection<Polygon> toJtsPolygons(Area area) {
if (area.isEmpty()) {
return Collections.emptyList();
}

return toPolygonizer(area).getPolygons();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.prep.PreparedGeometry;
import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
import org.locationtech.jts.operation.valid.IsValidOp;

public abstract class AbstractAStarWalker extends AbstractZoneWalker {
private record TerrainModifier(Token.TerrainModifierOperation operation, double value) {}
Expand Down Expand Up @@ -244,17 +243,7 @@ protected List<CellPoint> calculatePath(CellPoint start, CellPoint goal) {
this.vblGeometry = null;
} else {
try {
var vblGeometry = GeometryUtil.toJts(vbl);

// polygons
if (!vblGeometry.isValid()) {
log.info(
"vblGeometry is invalid! May cause issues. Check for self-intersecting polygons.");
log.debug("Invalid vblGeometry: " + new IsValidOp(vblGeometry).getValidationError());
}

vblGeometry = vblGeometry.buffer(1); // .buffer always creates valid geometry.
this.vblGeometry = PreparedGeometryFactory.prepare(vblGeometry);
this.vblGeometry = PreparedGeometryFactory.prepare(GeometryUtil.toJts(vbl));
} catch (Exception e) {
log.info("vblGeometry oh oh: ", e);
}
Expand All @@ -272,20 +261,8 @@ protected List<CellPoint> calculatePath(CellPoint start, CellPoint goal) {
this.fowExposedAreaGeometry = null;
} else {
try {
var fowExposedAreaGeometry = GeometryUtil.toJts(fowExposedArea);

// polygons
if (!fowExposedAreaGeometry.isValid()) {
log.info(
"FoW Geometry is invalid! May cause issues. Check for self-intersecting polygons.");
log.debug(
"Invalid FoW Geometry: "
+ new IsValidOp(fowExposedAreaGeometry).getValidationError());
}

fowExposedAreaGeometry =
fowExposedAreaGeometry.buffer(1); // .buffer always creates valid geometry.
this.fowExposedAreaGeometry = PreparedGeometryFactory.prepare(fowExposedAreaGeometry);
this.fowExposedAreaGeometry =
PreparedGeometryFactory.prepare(GeometryUtil.toJts(fowExposedArea));
} catch (Exception e) {
log.info("FoW Geometry oh oh: ", e);
}
Expand Down
168 changes: 47 additions & 121 deletions src/main/java/net/rptools/maptool/server/Mapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;
import net.rptools.maptool.model.*;
import net.rptools.maptool.model.drawing.*;
import net.rptools.maptool.server.proto.*;
import net.rptools.maptool.server.proto.drawing.*;
import org.apache.logging.log4j.LogManager;
Expand All @@ -36,118 +34,46 @@ public class Mapper {
private static final Logger log = LogManager.getLogger(Mapper.class);

public static Area map(AreaDto areaDto) {
var segmentIterator = areaDto.getSegmentsList().iterator();
if (!segmentIterator.hasNext()) return new Area();
final var segments = areaDto.getSegmentsList();
final var path = new Path2D.Double(areaDto.getWindingValue(), segments.size());

var it =
new PathIterator() {
private SegmentDto currentSegment = segmentIterator.next();

@Override
public int getWindingRule() {
return areaDto.getWindingValue();
}

@Override
public boolean isDone() {
return !segmentIterator.hasNext();
}

@Override
public void next() {
currentSegment = segmentIterator.next();
}

@Override
public int currentSegment(float[] coords) {
switch (currentSegment.getSegmentTypeCase()) {
case MOVE_TO -> {
var segment = currentSegment.getMoveTo();
var point0 = segment.getPoint0();
coords[0] = (float) point0.getX();
coords[1] = (float) point0.getY();
return PathIterator.SEG_MOVETO;
}
case LINE_TO -> {
var segment = currentSegment.getLineTo();
var point0 = segment.getPoint0();
coords[0] = (float) point0.getX();
coords[1] = (float) point0.getY();
return PathIterator.SEG_LINETO;
}
case QUAD_TO -> {
var segment = currentSegment.getQuadTo();
var point0 = segment.getPoint0();
coords[0] = (float) point0.getX();
coords[1] = (float) point0.getY();
var point1 = segment.getPoint1();
coords[2] = (float) point1.getX();
coords[3] = (float) point1.getY();
return PathIterator.SEG_QUADTO;
}
case CUBIC_TO -> {
var segment = currentSegment.getCubicTo();
var point0 = segment.getPoint0();
coords[0] = (float) point0.getX();
coords[1] = (float) point0.getY();
var point1 = segment.getPoint1();
coords[2] = (float) point1.getX();
coords[3] = (float) point1.getY();
var point2 = segment.getPoint2();
coords[4] = (float) point2.getX();
coords[5] = (float) point2.getY();
return PathIterator.SEG_CUBICTO;
}
}
return SEG_CLOSE;
}
for (final SegmentDto currentSegment : areaDto.getSegmentsList()) {
switch (currentSegment.getSegmentTypeCase()) {
case MOVE_TO -> {
final var segment = currentSegment.getMoveTo();
var point = segment.getPoint0();
path.moveTo(point.getX(), point.getY());
}
case LINE_TO -> {
final var segment = currentSegment.getLineTo();
var point = segment.getPoint0();
path.lineTo(point.getX(), point.getY());
}
case QUAD_TO -> {
final var segment = currentSegment.getQuadTo();
var point0 = segment.getPoint0();
var point1 = segment.getPoint1();
path.quadTo(point0.getX(), point0.getY(), point1.getX(), point1.getY());
}
case CUBIC_TO -> {
final var segment = currentSegment.getCubicTo();
var point0 = segment.getPoint0();
var point1 = segment.getPoint1();
var point2 = segment.getPoint2();
path.curveTo(
point0.getX(),
point0.getY(),
point1.getX(),
point1.getY(),
point2.getX(),
point2.getY());
}
case CLOSE -> {
path.closePath();
}
}
}

@Override
public int currentSegment(double[] coords) {
switch (currentSegment.getSegmentTypeCase()) {
case MOVE_TO -> {
var segment = currentSegment.getMoveTo();
var point0 = segment.getPoint0();
coords[0] = point0.getX();
coords[1] = point0.getY();
return PathIterator.SEG_MOVETO;
}
case LINE_TO -> {
var segment = currentSegment.getLineTo();
var point0 = segment.getPoint0();
coords[0] = point0.getX();
coords[1] = point0.getY();
return PathIterator.SEG_LINETO;
}
case QUAD_TO -> {
var segment = currentSegment.getQuadTo();
var point0 = segment.getPoint0();
coords[0] = point0.getX();
coords[1] = point0.getY();
var point1 = segment.getPoint1();
coords[2] = point1.getX();
coords[3] = point1.getY();
return PathIterator.SEG_QUADTO;
}
case CUBIC_TO -> {
var segment = currentSegment.getCubicTo();
var point0 = segment.getPoint0();
coords[0] = point0.getX();
coords[1] = point0.getY();
var point1 = segment.getPoint1();
coords[2] = point1.getX();
coords[3] = point1.getY();
var point2 = segment.getPoint2();
coords[4] = point2.getX();
coords[5] = point2.getY();
return PathIterator.SEG_CUBICTO;
}
}
return SEG_CLOSE;
}
};
var path = new Path2D.Float();
path.append(it, false);
return new Area(path);
}

Expand All @@ -157,32 +83,32 @@ public static AreaDto map(Area area) {
var builder = AreaDto.newBuilder();

var it = area.getPathIterator(null);
float[] floats = new float[6];
double[] coords = new double[6];
builder.setWinding(AreaDto.WindingRule.forNumber(it.getWindingRule()));

for (; !it.isDone(); it.next()) {
var segmentBuilder = SegmentDto.newBuilder();
switch (it.currentSegment(floats)) {
switch (it.currentSegment(coords)) {
case PathIterator.SEG_MOVETO -> {
var point0Builder = DoublePointDto.newBuilder().setX(floats[0]).setY(floats[1]);
var point0Builder = DoublePointDto.newBuilder().setX(coords[0]).setY(coords[1]);
var moveTo = MoveToSegment.newBuilder().setPoint0(point0Builder);
segmentBuilder.setMoveTo(moveTo);
}
case PathIterator.SEG_LINETO -> {
var point0Builder = DoublePointDto.newBuilder().setX(floats[0]).setY(floats[1]);
var point0Builder = DoublePointDto.newBuilder().setX(coords[0]).setY(coords[1]);
var lineTo = LineToSegment.newBuilder().setPoint0(point0Builder);
segmentBuilder.setLineTo(lineTo);
}
case PathIterator.SEG_QUADTO -> {
var point0Builder = DoublePointDto.newBuilder().setX(floats[0]).setY(floats[1]);
var point1Builder = DoublePointDto.newBuilder().setX(floats[2]).setY(floats[3]);
var point0Builder = DoublePointDto.newBuilder().setX(coords[0]).setY(coords[1]);
var point1Builder = DoublePointDto.newBuilder().setX(coords[2]).setY(coords[3]);
var quadTo = QuadToSegment.newBuilder().setPoint0(point0Builder).setPoint1(point1Builder);
segmentBuilder.setQuadTo(quadTo);
}
case PathIterator.SEG_CUBICTO -> {
var point0Builder = DoublePointDto.newBuilder().setX(floats[0]).setY(floats[1]);
var point1Builder = DoublePointDto.newBuilder().setX(floats[2]).setY(floats[3]);
var point2Builder = DoublePointDto.newBuilder().setX(floats[4]).setY(floats[5]);
var point0Builder = DoublePointDto.newBuilder().setX(coords[0]).setY(coords[1]);
var point1Builder = DoublePointDto.newBuilder().setX(coords[2]).setY(coords[3]);
var point2Builder = DoublePointDto.newBuilder().setX(coords[4]).setY(coords[5]);
var cubicTo =
CubicToSegment.newBuilder()
.setPoint0(point0Builder)
Expand Down
Loading
Loading