18
18
import java .awt .Rectangle ;
19
19
import java .awt .geom .Area ;
20
20
import java .util .ArrayList ;
21
- import java .util .Collections ;
22
- import java .util .Comparator ;
23
21
import java .util .EnumMap ;
24
- import java .util .HashMap ;
25
22
import java .util .HashSet ;
26
- import java .util .IdentityHashMap ;
27
23
import java .util .List ;
28
24
import java .util .Map ;
29
25
import java .util .Set ;
30
26
import java .util .function .Consumer ;
31
27
import javax .annotation .Nonnull ;
32
- import javax .annotation .Nullable ;
33
28
import net .rptools .lib .CodeTimer ;
34
29
import net .rptools .lib .GeometryUtil ;
35
30
import net .rptools .maptool .client .AppUtil ;
36
31
import net .rptools .maptool .client .MapTool ;
37
32
import net .rptools .maptool .client .ui .zone .renderer .ZoneRenderer ;
38
33
import net .rptools .maptool .client .ui .zone .vbl .AreaTree ;
39
- import net .rptools .maptool .client .ui .zone .vbl .VisibilitySweepEndpoint ;
34
+ import net .rptools .maptool .client .ui .zone .vbl .VisibilityProblem ;
40
35
import net .rptools .maptool .client .ui .zone .vbl .VisionBlockingAccumulator ;
41
36
import net .rptools .maptool .model .AbstractPoint ;
42
37
import net .rptools .maptool .model .CellPoint ;
51
46
import net .rptools .maptool .model .player .Player .Role ;
52
47
import org .apache .logging .log4j .LogManager ;
53
48
import org .apache .logging .log4j .Logger ;
54
- import org .locationtech .jts .algorithm .Orientation ;
55
49
import org .locationtech .jts .awt .ShapeWriter ;
56
50
import org .locationtech .jts .geom .Coordinate ;
57
51
import org .locationtech .jts .geom .Geometry ;
58
52
import org .locationtech .jts .geom .GeometryFactory ;
59
- import org .locationtech .jts .geom .LineSegment ;
60
- import org .locationtech .jts .geom .LineString ;
61
- import org .locationtech .jts .geom .Polygon ;
62
- import org .locationtech .jts .geom .prep .PreparedGeometry ;
63
53
import org .locationtech .jts .geom .prep .PreparedGeometryFactory ;
64
- import org .locationtech .jts .geom .util .LineStringExtracter ;
65
- import org .locationtech .jts .operation .union .UnaryUnionOp ;
66
54
67
55
public class FogUtil {
68
56
private static final Logger log = LogManager .getLogger (FogUtil .class );
@@ -103,6 +91,9 @@ public class FogUtil {
103
91
104
92
List <Geometry > visibleAreas = new ArrayList <>();
105
93
for (final var topology : topologies .entrySet ()) {
94
+ final var solver =
95
+ new VisibilityProblem (
96
+ geometryFactory , new Coordinate (origin .getX (), origin .getY ()), visionGeometry );
106
97
final var accumulator =
107
98
new VisionBlockingAccumulator (geometryFactory , origin , visionGeometry );
108
99
final var isVisionCompletelyBlocked = accumulator .add (topology .getKey (), topology .getValue ());
@@ -111,11 +102,11 @@ public class FogUtil {
111
102
return new Area ();
112
103
}
113
104
114
- final var visibleArea =
115
- calculateVisibleArea (
116
- new Coordinate ( origin . getX (), origin . getY ()),
117
- accumulator . getVisionBlockingSegments (),
118
- visionGeometry );
105
+ for ( var string : accumulator . getVisionBlockingSegments ()) {
106
+ solver . add ( string );
107
+ }
108
+
109
+ final var visibleArea = solver . solve ( );
119
110
if (visibleArea != null ) {
120
111
visibleAreas .add (visibleArea );
121
112
}
@@ -136,177 +127,6 @@ public class FogUtil {
136
127
return vision ;
137
128
}
138
129
139
- private record NearestWallResult (LineSegment wall , Coordinate point , double distance ) {}
140
-
141
- private static NearestWallResult findNearestOpenWall (
142
- Set <LineSegment > openWalls , LineSegment ray ) {
143
- assert !openWalls .isEmpty ();
144
-
145
- @ Nullable LineSegment currentNearest = null ;
146
- @ Nullable Coordinate currentNearestPoint = null ;
147
- double nearestDistance = Double .MAX_VALUE ;
148
- for (final var openWall : openWalls ) {
149
- final var intersection = ray .lineIntersection (openWall );
150
- if (intersection == null ) {
151
- continue ;
152
- }
153
-
154
- final var distance = ray .p0 .distance (intersection );
155
- if (distance < nearestDistance ) {
156
- currentNearest = openWall ;
157
- currentNearestPoint = intersection ;
158
- nearestDistance = distance ;
159
- }
160
- }
161
-
162
- assert currentNearest != null ;
163
- return new NearestWallResult (currentNearest , currentNearestPoint , nearestDistance );
164
- }
165
-
166
- /**
167
- * Builds a list of endpoints for the sweep algorithm to consume.
168
- *
169
- * <p>The endpoints will be unique (i.e., no coordinate is represented more than once) and in a
170
- * consistent orientation (i.e., counterclockwise around the origin). In addition, all endpoints
171
- * will have their starting and ending walls filled according to which walls are incident to the
172
- * corresponding point.
173
- *
174
- * @param origin The center of vision, by which orientation can be determined.
175
- * @param visionBlockingSegments The "walls" that are able to block vision. All points in these
176
- * walls will be present in the returned list.
177
- * @return A list of all endpoints in counterclockwise order.
178
- */
179
- private static List <VisibilitySweepEndpoint > getSweepEndpoints (
180
- Coordinate origin , List <LineString > visionBlockingSegments ) {
181
- final Map <Coordinate , VisibilitySweepEndpoint > endpointsByPosition = new HashMap <>();
182
- for (final var segment : visionBlockingSegments ) {
183
- VisibilitySweepEndpoint current = null ;
184
- for (final var coordinate : segment .getCoordinates ()) {
185
- final var previous = current ;
186
- current =
187
- endpointsByPosition .computeIfAbsent (
188
- coordinate , c -> new VisibilitySweepEndpoint (c , origin ));
189
- if (previous == null ) {
190
- // We just started this segment; still need a second point.
191
- continue ;
192
- }
193
-
194
- final var isForwards =
195
- Orientation .COUNTERCLOCKWISE
196
- == Orientation .index (origin , previous .getPoint (), current .getPoint ());
197
- // Make sure the wall always goes in the counterclockwise direction.
198
- final LineSegment wall =
199
- isForwards
200
- ? new LineSegment (previous .getPoint (), coordinate )
201
- : new LineSegment (coordinate , previous .getPoint ());
202
- if (isForwards ) {
203
- previous .startsWall (wall );
204
- current .endsWall (wall );
205
- } else {
206
- previous .endsWall (wall );
207
- current .startsWall (wall );
208
- }
209
- }
210
- }
211
- final List <VisibilitySweepEndpoint > endpoints = new ArrayList <>(endpointsByPosition .values ());
212
-
213
- endpoints .sort (
214
- Comparator .comparingDouble (VisibilitySweepEndpoint ::getPseudoangle )
215
- .thenComparing (VisibilitySweepEndpoint ::getDistance ));
216
-
217
- return endpoints ;
218
- }
219
-
220
- private static @ Nullable Geometry calculateVisibleArea (
221
- Coordinate origin , List <LineString > visionBlockingSegments , PreparedGeometry visionGeometry ) {
222
- if (visionBlockingSegments .isEmpty ()) {
223
- // No topology, apparently.
224
- return null ;
225
- }
226
-
227
- /*
228
- * Unioning all the line segments has the nice effect of noding any intersections between line
229
- * segments. Without this, it may not be valid.
230
- * Note: if the geometry were only composed of one topology, it would certainly be valid due to
231
- * its "flat" nature. But even in that case, it is more robust to due the union in case this
232
- * flatness assumption ever changes.
233
- */
234
- final var allWallGeometry = new UnaryUnionOp (visionBlockingSegments ).union ();
235
- // Replace the original geometry with the well-defined geometry.
236
- visionBlockingSegments = new ArrayList <>();
237
- LineStringExtracter .getLines (allWallGeometry , visionBlockingSegments );
238
-
239
- /*
240
- * The algorithm requires walls in every direction. The easiest way to accomplish this is to add
241
- * the boundary of the bounding box.
242
- */
243
- final var envelope = allWallGeometry .getEnvelopeInternal ();
244
- envelope .expandToInclude (visionGeometry .getGeometry ().getEnvelopeInternal ());
245
- // Exact expansion distance doesn't matter, we just don't want the boundary walls to overlap
246
- // endpoints from real walls.
247
- envelope .expandBy (1.0 );
248
- // Because we definitely have geometry, the envelope will always be a non-trivial rectangle.
249
- visionBlockingSegments .add (((Polygon ) geometryFactory .toGeometry (envelope )).getExteriorRing ());
250
-
251
- // Now that we have valid geometry and a bounding box, we can continue with the sweep.
252
-
253
- final var endpoints = getSweepEndpoints (origin , visionBlockingSegments );
254
- Set <LineSegment > openWalls = Collections .newSetFromMap (new IdentityHashMap <>());
255
-
256
- // This initial sweep just makes sure we have the correct open set to start.
257
- for (final var endpoint : endpoints ) {
258
- openWalls .addAll (endpoint .getStartsWalls ());
259
- openWalls .removeAll (endpoint .getEndsWalls ());
260
- }
261
-
262
- // Now for the real sweep. Make sure to process the first point once more at the end to ensure
263
- // the sweep covers the full 360 degrees.
264
- endpoints .add (endpoints .get (0 ));
265
- List <Coordinate > visionPoints = new ArrayList <>();
266
- for (final var endpoint : endpoints ) {
267
- assert !openWalls .isEmpty ();
268
-
269
- final var ray = new LineSegment (origin , endpoint .getPoint ());
270
- final var nearestWallResult = findNearestOpenWall (openWalls , ray );
271
-
272
- openWalls .addAll (endpoint .getStartsWalls ());
273
- openWalls .removeAll (endpoint .getEndsWalls ());
274
-
275
- // Find a new nearest wall.
276
- final var newNearestWallResult = findNearestOpenWall (openWalls , ray );
277
-
278
- if (newNearestWallResult .wall != nearestWallResult .wall ) {
279
- // Implies we have changed which wall we are at. Need to figure out projections.
280
-
281
- if (openWalls .contains (nearestWallResult .wall ())) {
282
- // The previous nearest wall is still open. I.e., we didn't fall of its end but
283
- // encountered a new closer wall. So we project the current point to the previous
284
- // nearest wall, then step to the current point.
285
- visionPoints .add (nearestWallResult .point ());
286
- visionPoints .add (endpoint .getPoint ());
287
- } else {
288
- // The previous nearest wall is now closed. I.e., we "fell off" it and therefore have
289
- // encountered a different wall. So we step from the current point (which is on the
290
- // previous wall) to the projection on the new wall.
291
- visionPoints .add (endpoint .getPoint ());
292
- // Special case: if the two walls are adjacent, they share the current point. We don't
293
- // need to add the point twice, so just skip in that case.
294
- if (!endpoint .getStartsWalls ().contains (newNearestWallResult .wall ())) {
295
- visionPoints .add (newNearestWallResult .point ());
296
- }
297
- }
298
- }
299
- }
300
- if (visionPoints .size () < 3 ) {
301
- // This shouldn't happen, but just in case.
302
- log .warn ("Sweep produced too few points: {}" , visionPoints );
303
- return null ;
304
- }
305
- visionPoints .add (visionPoints .get (0 )); // Ensure a closed loop.
306
-
307
- return geometryFactory .createPolygon (visionPoints .toArray (Coordinate []::new ));
308
- }
309
-
310
130
/**
311
131
* Expose visible area and previous path of all tokens in the token set. Server and clients are
312
132
* updated.
0 commit comments