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
- import java .util .HashMap ;
21
+ import java .util .EnumMap ;
24
22
import java .util .HashSet ;
25
- import java .util .IdentityHashMap ;
26
23
import java .util .List ;
27
24
import java .util .Map ;
28
25
import java .util .Set ;
29
26
import java .util .function .Consumer ;
30
- import java .util .function .Function ;
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 );
@@ -73,13 +61,13 @@ public class FogUtil {
73
61
*
74
62
* @param origin the vision origin.
75
63
* @param vision the lightSourceArea.
76
- * @param topology the VBL topology.
64
+ * @param wallVbl the VBL topology.
77
65
* @return the visible area.
78
66
*/
79
67
public static @ Nonnull Area calculateVisibility (
80
68
Point origin ,
81
69
Area vision ,
82
- AreaTree topology ,
70
+ AreaTree wallVbl ,
83
71
AreaTree hillVbl ,
84
72
AreaTree pitVbl ,
85
73
AreaTree coverVbl ) {
@@ -94,26 +82,31 @@ public class FogUtil {
94
82
* cannot handle. These cases do not exist within a single type of topology, but can arise when
95
83
* we combine them.
96
84
*/
85
+
86
+ var topologies = new EnumMap <Zone .TopologyType , AreaTree >(Zone .TopologyType .class );
87
+ topologies .put (Zone .TopologyType .WALL_VBL , wallVbl );
88
+ topologies .put (Zone .TopologyType .HILL_VBL , hillVbl );
89
+ topologies .put (Zone .TopologyType .PIT_VBL , pitVbl );
90
+ topologies .put (Zone .TopologyType .COVER_VBL , coverVbl );
91
+
97
92
List <Geometry > visibleAreas = new ArrayList <>();
98
- final List <Function <VisionBlockingAccumulator , Boolean >> topologyConsumers = new ArrayList <>();
99
- topologyConsumers .add (acc -> acc .addWallBlocking (topology ));
100
- topologyConsumers .add (acc -> acc .addHillBlocking (hillVbl ));
101
- topologyConsumers .add (acc -> acc .addPitBlocking (pitVbl ));
102
- topologyConsumers .add (acc -> acc .addCoverBlocking (coverVbl ));
103
- for (final var consumer : topologyConsumers ) {
93
+ for (final var topology : topologies .entrySet ()) {
94
+ final var solver =
95
+ new VisibilityProblem (
96
+ geometryFactory , new Coordinate (origin .getX (), origin .getY ()), visionGeometry );
104
97
final var accumulator =
105
98
new VisionBlockingAccumulator (geometryFactory , origin , visionGeometry );
106
- final var isVisionCompletelyBlocked = consumer . apply ( accumulator );
99
+ final var isVisionCompletelyBlocked = accumulator . add ( topology . getKey (), topology . getValue () );
107
100
if (!isVisionCompletelyBlocked ) {
108
101
// Vision has been completely blocked by this topology. Short circuit.
109
102
return new Area ();
110
103
}
111
104
112
- final var visibleArea =
113
- calculateVisibleArea (
114
- new Coordinate ( origin . getX (), origin . getY ()),
115
- accumulator . getVisionBlockingSegments (),
116
- visionGeometry );
105
+ for ( var string : accumulator . getVisionBlockingSegments ()) {
106
+ solver . add ( string );
107
+ }
108
+
109
+ final var visibleArea = solver . solve ( );
117
110
if (visibleArea != null ) {
118
111
visibleAreas .add (visibleArea );
119
112
}
@@ -134,177 +127,6 @@ public class FogUtil {
134
127
return vision ;
135
128
}
136
129
137
- private record NearestWallResult (LineSegment wall , Coordinate point , double distance ) {}
138
-
139
- private static NearestWallResult findNearestOpenWall (
140
- Set <LineSegment > openWalls , LineSegment ray ) {
141
- assert !openWalls .isEmpty ();
142
-
143
- @ Nullable LineSegment currentNearest = null ;
144
- @ Nullable Coordinate currentNearestPoint = null ;
145
- double nearestDistance = Double .MAX_VALUE ;
146
- for (final var openWall : openWalls ) {
147
- final var intersection = ray .lineIntersection (openWall );
148
- if (intersection == null ) {
149
- continue ;
150
- }
151
-
152
- final var distance = ray .p0 .distance (intersection );
153
- if (distance < nearestDistance ) {
154
- currentNearest = openWall ;
155
- currentNearestPoint = intersection ;
156
- nearestDistance = distance ;
157
- }
158
- }
159
-
160
- assert currentNearest != null ;
161
- return new NearestWallResult (currentNearest , currentNearestPoint , nearestDistance );
162
- }
163
-
164
- /**
165
- * Builds a list of endpoints for the sweep algorithm to consume.
166
- *
167
- * <p>The endpoints will be unique (i.e., no coordinate is represented more than once) and in a
168
- * consistent orientation (i.e., counterclockwise around the origin). In addition, all endpoints
169
- * will have their starting and ending walls filled according to which walls are incident to the
170
- * corresponding point.
171
- *
172
- * @param origin The center of vision, by which orientation can be determined.
173
- * @param visionBlockingSegments The "walls" that are able to block vision. All points in these
174
- * walls will be present in the returned list.
175
- * @return A list of all endpoints in counterclockwise order.
176
- */
177
- private static List <VisibilitySweepEndpoint > getSweepEndpoints (
178
- Coordinate origin , List <LineString > visionBlockingSegments ) {
179
- final Map <Coordinate , VisibilitySweepEndpoint > endpointsByPosition = new HashMap <>();
180
- for (final var segment : visionBlockingSegments ) {
181
- VisibilitySweepEndpoint current = null ;
182
- for (final var coordinate : segment .getCoordinates ()) {
183
- final var previous = current ;
184
- current =
185
- endpointsByPosition .computeIfAbsent (
186
- coordinate , c -> new VisibilitySweepEndpoint (c , origin ));
187
- if (previous == null ) {
188
- // We just started this segment; still need a second point.
189
- continue ;
190
- }
191
-
192
- final var isForwards =
193
- Orientation .COUNTERCLOCKWISE
194
- == Orientation .index (origin , previous .getPoint (), current .getPoint ());
195
- // Make sure the wall always goes in the counterclockwise direction.
196
- final LineSegment wall =
197
- isForwards
198
- ? new LineSegment (previous .getPoint (), coordinate )
199
- : new LineSegment (coordinate , previous .getPoint ());
200
- if (isForwards ) {
201
- previous .startsWall (wall );
202
- current .endsWall (wall );
203
- } else {
204
- previous .endsWall (wall );
205
- current .startsWall (wall );
206
- }
207
- }
208
- }
209
- final List <VisibilitySweepEndpoint > endpoints = new ArrayList <>(endpointsByPosition .values ());
210
-
211
- endpoints .sort (
212
- Comparator .comparingDouble (VisibilitySweepEndpoint ::getPseudoangle )
213
- .thenComparing (VisibilitySweepEndpoint ::getDistance ));
214
-
215
- return endpoints ;
216
- }
217
-
218
- private static @ Nullable Geometry calculateVisibleArea (
219
- Coordinate origin , List <LineString > visionBlockingSegments , PreparedGeometry visionGeometry ) {
220
- if (visionBlockingSegments .isEmpty ()) {
221
- // No topology, apparently.
222
- return null ;
223
- }
224
-
225
- /*
226
- * Unioning all the line segments has the nice effect of noding any intersections between line
227
- * segments. Without this, it may not be valid.
228
- * Note: if the geometry were only composed of one topology, it would certainly be valid due to
229
- * its "flat" nature. But even in that case, it is more robust to due the union in case this
230
- * flatness assumption ever changes.
231
- */
232
- final var allWallGeometry = new UnaryUnionOp (visionBlockingSegments ).union ();
233
- // Replace the original geometry with the well-defined geometry.
234
- visionBlockingSegments = new ArrayList <>();
235
- LineStringExtracter .getLines (allWallGeometry , visionBlockingSegments );
236
-
237
- /*
238
- * The algorithm requires walls in every direction. The easiest way to accomplish this is to add
239
- * the boundary of the bounding box.
240
- */
241
- final var envelope = allWallGeometry .getEnvelopeInternal ();
242
- envelope .expandToInclude (visionGeometry .getGeometry ().getEnvelopeInternal ());
243
- // Exact expansion distance doesn't matter, we just don't want the boundary walls to overlap
244
- // endpoints from real walls.
245
- envelope .expandBy (1.0 );
246
- // Because we definitely have geometry, the envelope will always be a non-trivial rectangle.
247
- visionBlockingSegments .add (((Polygon ) geometryFactory .toGeometry (envelope )).getExteriorRing ());
248
-
249
- // Now that we have valid geometry and a bounding box, we can continue with the sweep.
250
-
251
- final var endpoints = getSweepEndpoints (origin , visionBlockingSegments );
252
- Set <LineSegment > openWalls = Collections .newSetFromMap (new IdentityHashMap <>());
253
-
254
- // This initial sweep just makes sure we have the correct open set to start.
255
- for (final var endpoint : endpoints ) {
256
- openWalls .addAll (endpoint .getStartsWalls ());
257
- openWalls .removeAll (endpoint .getEndsWalls ());
258
- }
259
-
260
- // Now for the real sweep. Make sure to process the first point once more at the end to ensure
261
- // the sweep covers the full 360 degrees.
262
- endpoints .add (endpoints .get (0 ));
263
- List <Coordinate > visionPoints = new ArrayList <>();
264
- for (final var endpoint : endpoints ) {
265
- assert !openWalls .isEmpty ();
266
-
267
- final var ray = new LineSegment (origin , endpoint .getPoint ());
268
- final var nearestWallResult = findNearestOpenWall (openWalls , ray );
269
-
270
- openWalls .addAll (endpoint .getStartsWalls ());
271
- openWalls .removeAll (endpoint .getEndsWalls ());
272
-
273
- // Find a new nearest wall.
274
- final var newNearestWallResult = findNearestOpenWall (openWalls , ray );
275
-
276
- if (newNearestWallResult .wall != nearestWallResult .wall ) {
277
- // Implies we have changed which wall we are at. Need to figure out projections.
278
-
279
- if (openWalls .contains (nearestWallResult .wall ())) {
280
- // The previous nearest wall is still open. I.e., we didn't fall of its end but
281
- // encountered a new closer wall. So we project the current point to the previous
282
- // nearest wall, then step to the current point.
283
- visionPoints .add (nearestWallResult .point ());
284
- visionPoints .add (endpoint .getPoint ());
285
- } else {
286
- // The previous nearest wall is now closed. I.e., we "fell off" it and therefore have
287
- // encountered a different wall. So we step from the current point (which is on the
288
- // previous wall) to the projection on the new wall.
289
- visionPoints .add (endpoint .getPoint ());
290
- // Special case: if the two walls are adjacent, they share the current point. We don't
291
- // need to add the point twice, so just skip in that case.
292
- if (!endpoint .getStartsWalls ().contains (newNearestWallResult .wall ())) {
293
- visionPoints .add (newNearestWallResult .point ());
294
- }
295
- }
296
- }
297
- }
298
- if (visionPoints .size () < 3 ) {
299
- // This shouldn't happen, but just in case.
300
- log .warn ("Sweep produced too few points: {}" , visionPoints );
301
- return null ;
302
- }
303
- visionPoints .add (visionPoints .get (0 )); // Ensure a closed loop.
304
-
305
- return geometryFactory .createPolygon (visionPoints .toArray (Coordinate []::new ));
306
- }
307
-
308
130
/**
309
131
* Expose visible area and previous path of all tokens in the token set. Server and clients are
310
132
* updated.
0 commit comments