-
Notifications
You must be signed in to change notification settings - Fork 335
/
Copy pathDirectedPath.java
522 lines (461 loc) · 19.7 KB
/
DirectedPath.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
/*
* Copyright (C) 2012 United States Government as represented by the Administrator of the
* National Aeronautics and Space Administration.
* All Rights Reserved.
*/
package gov.nasa.worldwindx.examples.util;
import com.jogamp.common.nio.Buffers;
import gov.nasa.worldwind.View;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.geom.Box;
import gov.nasa.worldwind.render.*;
import gov.nasa.worldwind.terrain.Terrain;
import gov.nasa.worldwind.util.*;
import javax.media.opengl.*;
import java.nio.*;
import java.util.List;
/**
* A {@link Path} that draws arrowheads between the path positions to indicate direction. All arrowheads are drawn at a
* constant geographic size (the arrows get smaller as the view moves away from the path, and larger as the view get
* closer to the path). One arrowhead is drawn on each path segment, unless the path segment is smaller than the
* arrowhead, in which case the arrowhead is not drawn.
*
* @author pabercrombie
* @version $Id: DirectedPath.java 3034 2015-04-17 18:04:14Z dcollins $
*/
public class DirectedPath extends Path
{
/** Default arrow offset (value between 0 and 1). */
public static final double DEFAULT_ARROW_OFFSET = 0.5;
/** Default arrow length, in meters. */
public static final double DEFAULT_ARROW_LENGTH = 300;
/** Default arrow angle. */
public static final Angle DEFAULT_ARROW_ANGLE = Angle.fromDegrees(45.0);
/** Default maximum screen size of the arrowheads, in pixels. */
public static final double DEFAULT_MAX_SCREEN_SIZE = 20.0;
/** The offset that determines where the arrow is positioned. */
protected double arrowOffset = DEFAULT_ARROW_OFFSET;
/** The length, in meters, of the arrowhead, from tip to base. */
protected double arrowLength = DEFAULT_ARROW_LENGTH;
/** The angle of the arrowhead tip. */
protected Angle arrowAngle = DEFAULT_ARROW_ANGLE;
/** The maximum screen size, in pixels, of the direction arrowheads. */
protected double maxScreenSize = DEFAULT_MAX_SCREEN_SIZE;
/** Creates a path with no positions. */
public DirectedPath()
{
super();
}
/**
* Creates a path with specified positions.
* <p/>
* Note: If fewer than two positions is specified, no path is drawn.
*
* @param positions the path positions. This reference is retained by this shape; the positions are not copied. If
* any positions in the set change, {@link #setPositions(Iterable)} must be called to inform this
* shape of the change.
*
* @throws IllegalArgumentException if positions is null.
*/
public DirectedPath(Iterable<? extends Position> positions)
{
super(positions);
}
/**
* Creates a path with positions specified via a generic list.
* <p/>
* Note: If fewer than two positions is specified, the path is not drawn.
*
* @param positions the path positions. This reference is retained by this shape; the positions are not copied. If
* any positions in the set change, {@link #setPositions(Iterable)} must be called to inform this
* shape of the change.
*
* @throws IllegalArgumentException if positions is null.
*/
public DirectedPath(Position.PositionList positions)
{
super(positions.list);
}
/**
* Creates a path between two positions.
*
* @param posA the first position.
* @param posB the second position.
*
* @throws IllegalArgumentException if either position is null.
*/
public DirectedPath(Position posA, Position posB)
{
super(posA, posB);
}
/**
* Indicates the offset (as a ratio) of the arrowhead from base (0.0) to tip (1.0).
*
* @return The offset of the direction arrowheads.
*/
public double getArrowOffset()
{
return this.arrowOffset;
}
/**
* Specifies the offset of the direction arrowheads, from base (0.0) to tip (1.0).
*
* @param arrowOffset offset of the direction arrowheads. The offset must be a value between 0.0 and 1.0.
*/
public void setArrowOffset(double arrowOffset)
{
if (arrowOffset < 0 || arrowOffset > 1.0)
{
String message = Logging.getMessage("generic.ArgumentOutOfRange", arrowOffset);
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.arrowOffset = arrowOffset;
}
/**
* Indicates the length, in meters, of the direction arrowheads, from base to tip.
*
* @return The geographic length of the direction arrowheads.
*/
public double getArrowLength()
{
return this.arrowLength;
}
/**
* Specifies the length, in meters, of the direction arrowheads, from base to tip.
*
* @param arrowLength length, in meters, of the direction arrowheads. The length must be greater than zero.
*/
public void setArrowLength(double arrowLength)
{
if (arrowLength <= 0)
{
String message = Logging.getMessage("generic.ArgumentOutOfRange", arrowLength);
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.arrowLength = arrowLength;
}
/**
* Indicates the maximum screen size, in pixels, of the direction arrowheads. The arrowheads are drawn a fixed
* geographic, but they are not allowed to get bigger than {@code maxScreenSize} pixels.
*
* @return The maximum screen size, in pixels, of the direction arrowheads, measured tip to base.
*/
public double getMaxScreenSize()
{
return this.maxScreenSize;
}
/**
* Specifies the maximum screen size, in pixels, of the direction arrowheads. The arrowheads are drawn at a fixed
* geographic size, but they will not allowed to get bigger than {@code maxScreenSize} pixels.
*
* @param maxScreenSize the maximum screen size, in pixels, of the direction arrowheads, measured tip to base.
*/
public void setMaxScreenSize(double maxScreenSize)
{
if (maxScreenSize <= 0)
{
String message = Logging.getMessage("generic.ArgumentOutOfRange", maxScreenSize);
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.maxScreenSize = maxScreenSize;
}
/**
* Indicates the angle of the direction arrowheads. A larger angle draws a fat arrowhead, and a smaller angle draws
* a narrow arrow head.
*
* @return The angle of the direction arrowhead tip.
*/
public Angle getArrowAngle()
{
return this.arrowAngle;
}
/**
* Specifies the angle of the direction arrowheads. A larger angle draws a fat arrowhead, and a smaller angle draws
* a narrow arrow.
*
* @param arrowAngle angle of the direction arrowhead tip. Valid values are between 0 degrees and 90 degrees.
*/
public void setArrowAngle(Angle arrowAngle)
{
if (arrowAngle == null)
{
String message = Logging.getMessage("nullValue.AngleIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
if ((arrowAngle.compareTo(Angle.ZERO) <= 0) || (arrowAngle.compareTo(Angle.POS90) >= 0))
{
String message = Logging.getMessage("generic.ArgumentOutOfRange", arrowAngle);
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.arrowAngle = arrowAngle;
}
protected static final String ARROWS_KEY = "DirectedPath.DirectionArrows";
protected static final String ARROWS_EXTENT = "DirectedPath.DirectionArrowsExtent";
protected boolean intersectsFrustum(DrawContext dc)
{
// Must override this method to account for the extent of the arrowheads.
boolean intersects = super.intersectsFrustum(dc);
if (intersects || !dc.isPickingMode())
{
return intersects;
}
Box box = (Box) this.currentData.getValue(ARROWS_EXTENT);
return box == null || dc.getPickFrustums().intersectsAny(box);
}
/**
* {@inheritDoc}
* <p/>
* Overridden to also compute the geometry of the direction arrows.
*/
@Override
protected void computePath(DrawContext dc, List<Position> positions, PathData pathData)
{
super.computePath(dc, positions, pathData);
// this.computeDirectionArrows(dc, pathData);
}
/**
* {@inheritDoc}
* <p/>
* Overridden to return a {@link gov.nasa.worldwindx.examples.util.DirectedSurfacePolyline}.
*/
@Override
protected SurfaceShape createSurfaceShape()
{
DirectedSurfacePolyline polyline = new DirectedSurfacePolyline();
polyline.setLocations(this.getPositions());
return polyline;
}
/**
* {@inheritDoc}
* <p/>
* Overridden to update the arrow properties of {@link gov.nasa.worldwindx.examples.util.DirectedSurfacePolyline}.
*/
@Override
protected void updateSurfaceShape()
{
super.updateSurfaceShape();
((DirectedSurfacePolyline) this.surfaceShape).setArrowLength(this.getArrowLength());
((DirectedSurfacePolyline) this.surfaceShape).setMaxScreenSize(this.getMaxScreenSize());
((DirectedSurfacePolyline) this.surfaceShape).setArrowAngle(this.getArrowAngle());
}
/**
* Compute the geometry of the direction arrows.
*
* @param dc current draw context.
* @param pathData the current globe-specific path data.
*/
protected void computeDirectionArrows(DrawContext dc, PathData pathData)
{
IntBuffer polePositions = pathData.getPolePositions();
int numPositions = polePositions.limit() / 2; // One arrow head for each path segment
List<Position> tessellatedPositions = pathData.getTessellatedPositions();
final int FLOATS_PER_ARROWHEAD = 9; // 3 points * 3 coordinates per point
FloatBuffer buffer = (FloatBuffer) pathData.getValue(ARROWS_KEY);
if (buffer == null || buffer.capacity() < numPositions * FLOATS_PER_ARROWHEAD)
{
buffer = Buffers.newDirectFloatBuffer(FLOATS_PER_ARROWHEAD * numPositions);
}
pathData.setValue(ARROWS_KEY, buffer);
buffer.clear();
Terrain terrain = dc.getTerrain();
// Step through polePositions to find the original path locations.
int poleA = polePositions.get(0) / 2;
Vec4 polePtA = this.computePoint(dc, terrain, tessellatedPositions.get(poleA));
// Draw one arrowhead for each segment in the original position list. The path may be tessellated,
// so we need to find the tessellated segment halfway between each pair of original positions.
// polePositions holds indices into the rendered path array of the original vertices. Step through
// polePositions by 2 because we only care about where the top of the pole is, not the bottom.
for (int i = 2; i < polePositions.limit(); i += 2)
{
// Find the position of this pole and the next pole. Divide by 2 to convert an index in the
// renderedPath buffer to a index in the tessellatedPositions list.
int poleB = polePositions.get(i) / 2;
Vec4 polePtB = this.computePoint(dc, terrain, tessellatedPositions.get(poleB));
this.computeArrowheadGeometry(dc, poleA, poleB, polePtA, polePtB, buffer, pathData);
poleA = poleB;
polePtA = polePtB;
}
buffer.flip();
// Create an extent for the arrowheads if we're picking.
if (dc.isPickingMode())
{
if (buffer.remaining() != 0)
{
Box box = Box.computeBoundingBox(new BufferWrapper.FloatBufferWrapper(buffer), 3);
box = box.translate(pathData.getReferencePoint());
pathData.setValue(ARROWS_EXTENT, box);
}
else
{
pathData.setValue(ARROWS_EXTENT, null);
}
}
}
/**
* Compute the geometry of a direction arrow between two points.
*
* @param dc current draw context
* @param polePtA the first pole position. This is one of the application defined path positions.
* @param polePtB second pole position
* @param buffer buffer in which to place computed points
* @param pathData the current globe-specific path data.
*/
protected void computeArrowheadGeometry(DrawContext dc, int poleA, int poleB, Vec4 polePtA, Vec4 polePtB,
FloatBuffer buffer, PathData pathData)
{
// Build a triangle to represent the arrowhead. The triangle is built from two vectors, one parallel to the
// segment, and one perpendicular to it. The plane of the arrowhead will be parallel to the surface.
double arrowLength = this.getArrowLength();
double arrowBase = arrowLength * this.getArrowAngle().tanHalfAngle();
double poleDistance = polePtA.distanceTo3(polePtB);
// Find the segment that is midway between the two poles.
int midIndex = (int)Math.floor(this.arrowOffset * (poleA + poleB));
List<Position> tessellatedPositions = pathData.getTessellatedPositions();
Position posA = tessellatedPositions.get(midIndex);
Position posB = tessellatedPositions.get(midIndex + 1);
Terrain terrain = dc.getTerrain();
Vec4 ptA = this.computePoint(dc, terrain, posA);
Vec4 ptB = this.computePoint(dc, terrain, posB);
// Compute parallel component
Vec4 parallel = ptA.subtract3(ptB);
// Compute perpendicular component
Vec4 surfaceNormal = dc.getGlobe().computeSurfaceNormalAtPoint(ptB);
Vec4 perpendicular = surfaceNormal.cross3(parallel);
// Compute midpoint of segment. When the number of segments is odd, the midpoint falls between two tessellated
// positions. When the number of segments is even, the midpoint falls on the middle tessellated position.
Vec4 midPoint;
if ((poleA - poleB) % 2 != 0)
{
midPoint = ptA.add3(ptB).divide3(2.0);
}
else
{
midPoint = ptA;
}
if (!this.isArrowheadSmall(dc, midPoint, 1))
{
// Compute the size of the arrowhead in pixels to ensure that the arrow does not exceed the maximum
// screen size.
View view = dc.getView();
double midpointDistance = view.getEyePoint().distanceTo3(midPoint);
double pixelSize = view.computePixelSizeAtDistance(midpointDistance);
if (arrowLength / pixelSize > this.maxScreenSize)
{
arrowLength = this.maxScreenSize * pixelSize;
arrowBase = arrowLength * this.getArrowAngle().tanHalfAngle();
}
// Don't draw an arrowhead if the path segment is smaller than the arrow
if (poleDistance <= arrowLength)
{
return;
}
perpendicular = perpendicular.normalize3().multiply3(arrowBase);
parallel = parallel.normalize3().multiply3(arrowLength);
// Center the arrow on the midpoint.
midPoint = midPoint.subtract3(parallel.divide3(2.0));
// Compute geometry of direction arrow
Vec4 vertex1 = midPoint.add3(parallel).add3(perpendicular);
Vec4 vertex2 = midPoint.add3(parallel).add3(perpendicular.multiply3(-1.0));
// Add geometry to the buffer
Vec4 referencePoint = pathData.getReferencePoint();
buffer.put((float) (vertex1.x - referencePoint.x));
buffer.put((float) (vertex1.y - referencePoint.y));
buffer.put((float) (vertex1.z - referencePoint.z));
buffer.put((float) (vertex2.x - referencePoint.x));
buffer.put((float) (vertex2.y - referencePoint.y));
buffer.put((float) (vertex2.z - referencePoint.z));
buffer.put((float) (midPoint.x - referencePoint.x));
buffer.put((float) (midPoint.y - referencePoint.y));
buffer.put((float) (midPoint.z - referencePoint.z));
}
}
//
// /** {@inheritDoc} */
// @Override
// protected boolean mustRegenerateGeometry(DrawContext dc)
// {
// // Path never regenerates geometry for absolute altitude mode paths, but the direction arrows in DirectedPath
// // need to be recomputed because the view may have changed and the size of the arrows needs to be recalculated.
// if (this.getCurrentPathData().isExpired(dc))
// return true;
//
// return super.mustRegenerateGeometry(dc);
// }
/**
* Determines if an direction arrow drawn a point will be less than a specified number of pixels.
*
* @param dc current draw context
* @param arrowPt point at which to draw direction arrow
* @param numPixels the number of pixels which is considered to be "small"
*
* @return {@code true} if an arrow drawn at {@code arrowPt} would occupy less than or equal to {@code numPixels}.
*/
protected boolean isArrowheadSmall(DrawContext dc, Vec4 arrowPt, int numPixels)
{
return this.getArrowLength() <= numPixels * dc.getView().computePixelSizeAtDistance(
dc.getView().getEyePoint().distanceTo3(arrowPt));
}
/**
* {@inheritDoc}
* <p/>
* Overridden to also draw direction arrows.
*
* @param dc Current draw context.
*/
@Override
protected void doDrawOutline(DrawContext dc)
{
this.computeDirectionArrows(dc, this.getCurrentPathData());
this.drawDirectionArrows(dc, this.getCurrentPathData());
super.doDrawOutline(dc);
}
/**
* Draws this DirectedPath's direction arrows. Called from {@link #doDrawOutline(gov.nasa.worldwind.render.DrawContext)}
* before drawing the Path's actual outline.
* <p/>
* If this Path is entirely located on the terrain, this applies an offset to the arrow's depth values to to ensure
* they shows over the terrain. This does not apply a depth offset in any other case to avoid incorrectly drawing
* the arrows over objects they should be behind, including the terrain. In addition to applying a depth offset,
* this disables writing to the depth buffer to avoid causing subsequently drawn ordered renderables to incorrectly
* fail the depth test. Since the arrows are located on the terrain, the terrain already provides the necessary
* depth values and we can be certain that other ordered renderables should appear on top of them.
*
* @param dc Current draw context.
* @param pathData the current globe-specific path data.
*/
protected void drawDirectionArrows(DrawContext dc, PathData pathData)
{
FloatBuffer points = (FloatBuffer) pathData.getValue(ARROWS_KEY);
if (points == null || points.remaining() == 0)
{
return;
}
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
boolean projectionOffsetPushed = false; // keep track for error recovery
try
{
if (this.isSurfacePath(dc))
{
// Pull the arrow triangles forward just a bit to ensure they show over the terrain.
dc.pushProjectionOffest(SURFACE_PATH_DEPTH_OFFSET);
gl.glDepthMask(false);
projectionOffsetPushed = true;
}
gl.glVertexPointer(3, GL.GL_FLOAT, 0, points);
gl.glDrawArrays(GL.GL_TRIANGLES, 0, points.remaining() / 3);
}
finally
{
if (projectionOffsetPushed)
{
dc.popProjectionOffest();
gl.glDepthMask(true);
}
}
}
}