Skip to content
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
35 changes: 26 additions & 9 deletions src/main/java/net/rptools/lib/GeometryUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/
package net.rptools.lib;

import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.Point2D;
import java.util.ArrayList;
Expand All @@ -31,19 +32,22 @@
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateArrays;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.Location;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.PrecisionModel;
import org.locationtech.jts.geom.util.GeometryFixer;
import org.locationtech.jts.operation.valid.IsValidOp;
import org.locationtech.jts.precision.GeometryPrecisionReducer;

public class GeometryUtil {
private static final Logger log = LogManager.getLogger(GeometryUtil.class);

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

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

Expand Down Expand Up @@ -124,21 +128,21 @@ public static GeometryFactory getGeometryFactory() {
return geometryFactory;
}

public static MultiPolygon toJts(Area area) {
final var polygons = toJtsPolygons(area);
public static MultiPolygon toJts(Shape shape) {
final var polygons = toJtsPolygons(shape);
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()) {
public static Collection<Polygon> toJtsPolygons(Shape shape) {
if (shape instanceof Area area && area.isEmpty()) {
return Collections.emptyList();
}

final var pathIterator = area.getPathIterator(null, 1. / precisionModel.getScale());
final var pathIterator = shape.getPathIterator(null, 1. / finePrecisionModel.getScale());
final var coordinates = (List<Coordinate[]>) ShapeReader.toCoordinates(pathIterator);

// Now collect all the noded rings into islands (JTS clockwise) and oceans (counterclockwise).
Expand All @@ -149,7 +153,13 @@ public static Collection<Polygon> toJtsPolygons(Area area) {
log.warn(
"Found invalid geometry: ring has only {} points but at least four are required.",
ring.length);
} else if (Orientation.isCCW(ring)) {
continue;
}

for (var c : ring) {
finePrecisionModel.makePrecise(c);
}
if (Orientation.isCCW(ring)) {
oceans.add(ring);
} else {
islands.add(new Island(geometryFactory, ring));
Expand Down Expand Up @@ -191,7 +201,14 @@ public static Collection<Polygon> toJtsPolygons(Area area) {
// Build the polygon...
var polygon = island.toPolygon();
// ... then make sure it is valid, fixing it if not.
var fixedPolygon = GeometryPrecisionReducer.reduce(polygon, precisionModel);
Geometry fixedPolygon;
try {
fixedPolygon = GeometryPrecisionReducer.reduce(GeometryFixer.fix(polygon), precisionModel);
} catch (Throwable t) {
log.error("Failure while reducing polygon", t);
continue;
}

switch (fixedPolygon) {
case Polygon p -> polygons.add(p);
case MultiPolygon mp -> {
Expand Down Expand Up @@ -220,8 +237,8 @@ public static Coordinate point2DToCoordinate(Point2D point2D) {
}

/**
* Represents intermediate results in {@link #toJtsPolygons(Area)} that allows attaching oceans to
* parent islands.
* Represents intermediate results in {@link #toJtsPolygons(Shape)} that allows attaching oceans
* to parent islands.
*
* <p>The ultimate result is a {@link Polygon} that can be obtained via {@link #toPolygon()}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.awt.Shape;
import java.awt.event.MouseEvent;
import java.awt.geom.Area;
import java.util.List;
import javax.annotation.Nullable;
import javax.swing.SwingUtilities;
import net.rptools.maptool.client.AppStatePersisted;
Expand Down Expand Up @@ -218,16 +219,27 @@ public void paintOverlay(ZoneRenderer renderer, Graphics2D g) {
g2.translate(renderer.getViewOffsetX(), renderer.getViewOffsetY());
g2.scale(renderer.getScale(), renderer.getScale());

var tokenMasks = zone.getTokenMaskTopologies(null);
g2.setColor(AppStyle.tokenMblColor);
g2.fill(zone.getTokenMaskTopology(Zone.TopologyType.MBL, null));
for (var topology : tokenMasks.getOrDefault(Zone.TopologyType.MBL, List.of())) {
g2.fill(topology);
}
g2.setColor(AppStyle.tokenTopologyColor);
g2.fill(zone.getTokenMaskTopology(Zone.TopologyType.WALL_VBL, null));
for (var topology : tokenMasks.getOrDefault(Zone.TopologyType.WALL_VBL, List.of())) {
g2.fill(topology);
}
g2.setColor(AppStyle.tokenHillVblColor);
g2.fill(zone.getTokenMaskTopology(Zone.TopologyType.HILL_VBL, null));
for (var topology : tokenMasks.getOrDefault(Zone.TopologyType.HILL_VBL, List.of())) {
g2.fill(topology);
}
g2.setColor(AppStyle.tokenPitVblColor);
g2.fill(zone.getTokenMaskTopology(Zone.TopologyType.PIT_VBL, null));
for (var topology : tokenMasks.getOrDefault(Zone.TopologyType.PIT_VBL, List.of())) {
g2.fill(topology);
}
g2.setColor(AppStyle.tokenCoverVblColor);
g2.fill(zone.getTokenMaskTopology(Zone.TopologyType.COVER_VBL, null));
for (var topology : tokenMasks.getOrDefault(Zone.TopologyType.COVER_VBL, List.of())) {
g2.fill(topology);
}

g2.setColor(AppStyle.topologyTerrainColor);
g2.fill(zone.getMaskTopology(Zone.TopologyType.MBL));
Expand Down
39 changes: 26 additions & 13 deletions src/main/java/net/rptools/maptool/model/Zone.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import net.rptools.lib.GeometryUtil;
import net.rptools.lib.MD5Key;
import net.rptools.maptool.client.AppUtil;
import net.rptools.maptool.client.MapTool;
Expand Down Expand Up @@ -967,19 +968,25 @@ public WallTopology getWalls() {
* All other tokens' topology will be included.
*
* @param types The type of masks to get.
* @param excluding
* @param excluding The ID of the token to exclude. Meant for pathfinding where a token should not
* consider its own MBL.
* @return
*/
public List<MaskTopology> getMasks(Set<TopologyType> types, @Nullable GUID excluding) {
var masks = new ArrayList<MaskTopology>();
var tokenMasks = getTokenMaskTopologies(excluding);

for (var type : types) {
var topologies = tokenMasks.getOrDefault(type, List.of());

var mapArea = getMaskTopology(type);
var tokenArea = getTokenMaskTopology(type, excluding);
if (mapArea != null) {
tokenArea.add(mapArea);
topologies.add(mapArea);
}
masks.addAll(MaskTopology.createFromLegacy(type, tokenArea));

var union = GeometryUtil.union(topologies);

masks.addAll(MaskTopology.createFromLegacy(type, union));
}

return masks;
Expand Down Expand Up @@ -1044,18 +1051,24 @@ public void tokenMaskTopologyChanged(Collection<TopologyType> types) {
new MapToolEventBus().getMainEventBus().post(new MaskTopologyChanged(this));
}

public Area getTokenMaskTopology(TopologyType type, @Nullable GUID excluding) {
var result = new Area();
for (var token : getAllTokens()) {
if (excluding != null && excluding.equals(token.getId())) {
continue;
}
public Map<TopologyType, List<Area>> getTokenMaskTopologies(@Nullable GUID excluding) {
var result = new EnumMap<TopologyType, List<Area>>(TopologyType.class);

var tokenArea = token.getTransformedMaskTopology(type);
if (tokenArea != null) {
result.add(tokenArea);
for (var type : TopologyType.values()) {
var topologies = new ArrayList<Area>();
for (var token : getAllTokens()) {
if (excluding != null && excluding.equals(token.getId())) {
continue;
}

var tokenArea = token.getTransformedMaskTopology(type);
if (tokenArea != null) {
topologies.add(tokenArea);
}
}
result.put(type, topologies);
}

return result;
}

Expand Down
105 changes: 102 additions & 3 deletions src/test/java/net/rptools/lib/GeometryUtilTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import static org.junit.jupiter.params.provider.Arguments.argumentSet;

import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.Path2D;
import java.util.ArrayList;
Expand All @@ -41,12 +42,12 @@ public class GeometryUtilTest {
@DisplayName(
"Verify that meaningful topology is returned when converting AWT Area to JTS Geometry")
@MethodSource("areaProvider")
void testConversionFromAreaToGeometry(Area area, List<Polygon> expectedPolygons) {
void testConversionFromAreaToGeometry(Shape shape, List<Polygon> expectedPolygons) {
var expectedGeometry =
geometryFactory.createMultiPolygon(expectedPolygons.toArray(Polygon[]::new));

Geometry geometry = GeometryUtil.toJts(area);
Collection<Polygon> polygons = GeometryUtil.toJtsPolygons(area);
Geometry geometry = GeometryUtil.toJts(shape);
Collection<Polygon> polygons = GeometryUtil.toJtsPolygons(shape);

assert expectedGeometry.equalsTopo(geometry) : "Geometry must have the correct topology";

Expand Down Expand Up @@ -135,6 +136,104 @@ private static Iterable<Arguments> areaProvider() {
}
// endregion

// region Octothorpe with several tiny bowties
{
final var path = new Path2D.Double();
{
path.moveTo(31096.89298687769800, 19958.84411439553500);
path.lineTo(31210.89219385643000, 20107.34783396915700);
path.lineTo(31210.89219385642700, 20107.34783396915700);
path.lineTo(31118.96156955349200, 20131.10263820686700);
path.lineTo(31118.96156955349600, 20131.10263820686700);
path.lineTo(31016.92922050016200, 20007.45592609495000);
path.lineTo(31096.89298687770500, 19958.84411439553500);
path.closePath();
}
{
path.moveTo(30938.47499442061200, 19745.56575078792600);
path.lineTo(30934.31191954007000, 19747.05410399316700);
path.lineTo(31093.02554099086000, 19953.80609602096600);
path.lineTo(31012.86872776624700, 20002.53526533626400);
path.lineTo(30825.28245459803000, 19775.21102817247800);
path.lineTo(30819.84768888939700, 19779.82141036563600);
path.lineTo(31006.71603098942800, 20006.27563113827000);
path.lineTo(30711.87783426228000, 20185.51454834266000);
path.lineTo(30714.85047572821700, 20191.09653889346700);
path.lineTo(31010.77652372334700, 20011.19629189695500);
path.lineTo(31111.34617477415700, 20133.07045049518300);
path.lineTo(31111.34617477416000, 20133.07045049518300);
path.lineTo(30771.12499411203700, 20220.98334791573000);
path.lineTo(30773.48922015583200, 20224.71923106947000);
path.lineTo(31114.30270195864400, 20136.65328347926000);
path.lineTo(31114.30270195864000, 20136.65328347926000);
path.lineTo(31232.07575184261800, 20279.37518029587600);
path.lineTo(31237.51051755124700, 20274.76479810271000);
path.lineTo(31121.91809673798000, 20134.68547119094400);
path.lineTo(31121.91809673797600, 20134.68547119094400);
path.lineTo(31213.67668806736000, 20110.97512014432500);
path.lineTo(31490.52127924277700, 20471.61316862705300);
path.lineTo(31494.68435412332000, 20470.12481542181200);
path.lineTo(31218.10407980696000, 20109.83108544582000);
path.lineTo(31657.87127955135300, 19996.19557149924700);
path.lineTo(31655.50705350755700, 19992.45968834550500);
path.lineTo(31215.31958559603000, 20106.20379927065000);
path.lineTo(31100.51043328870600, 19956.64498556843700);
path.lineTo(31100.51043328870200, 19956.64498556843700);
path.lineTo(31296.52685776363800, 19837.48209532163300);
path.lineTo(31293.55421629769600, 19831.90010477082300);
path.lineTo(31096.64298740186500, 19951.60696719386400);
path.lineTo(30938.47499442061200, 19745.56575078792600);
path.closePath();
}

final var polygons =
new Polygon[] {
createPrecisePolygon(
new Coordinate[] {
new Coordinate(30934.31192, 19747.0541),
new Coordinate(31093.02554, 19953.8061),
new Coordinate(31012.86873, 20002.53527),
new Coordinate(30825.28245, 19775.21103),
new Coordinate(30819.84769, 19779.82141),
new Coordinate(31006.71603, 20006.27563),
new Coordinate(30711.87783, 20185.51455),
new Coordinate(30714.85048, 20191.09654),
new Coordinate(31010.77652, 20011.19629),
new Coordinate(31111.34617, 20133.07045),
new Coordinate(30771.12499, 20220.98335),
new Coordinate(30773.48922, 20224.71923),
new Coordinate(31114.3027, 20136.65328),
new Coordinate(31232.07575, 20279.37518),
new Coordinate(31237.51052, 20274.7648),
new Coordinate(31121.9181, 20134.68547),
new Coordinate(31213.67669, 20110.97512),
new Coordinate(31490.52128, 20471.61317),
new Coordinate(31494.68435, 20470.12482),
new Coordinate(31218.10408, 20109.83109),
new Coordinate(31657.87128, 19996.19557),
new Coordinate(31655.50705, 19992.45969),
new Coordinate(31215.31959, 20106.2038),
new Coordinate(31100.51043, 19956.64499),
new Coordinate(31296.52686, 19837.4821),
new Coordinate(31293.55422, 19831.9001),
new Coordinate(31096.64299, 19951.60697),
new Coordinate(30938.47499, 19745.56575),
new Coordinate(30934.31192, 19747.0541),
},
new Coordinate[] {
new Coordinate(31016.92922, 20007.45593),
new Coordinate(31096.89299, 19958.84411),
new Coordinate(31210.89219, 20107.34783),
new Coordinate(31118.96157, 20131.10264),
new Coordinate(31016.92922, 20007.45593),
}),
};

argumentsList.add(
argumentSet("Octothorpe with several tiny bowties", path, List.of(polygons)));
}
// endregion

// region Geometry with a collapsible tiny polygon.
{
final var path = new Path2D.Double();
Expand Down