protected void makeTessellatedLocations( Globe globe, int subdivisions, List<LatLon> locations, List<LatLon> tessellatedLocations) { ArrayList<Vec4> points = new ArrayList<Vec4>(); for (LatLon ll : locations) { points.add(globe.computePointFromLocation(ll)); } //noinspection StringEquality if (WWMath.computeWindingOrderOfLocations(locations) != AVKey.COUNTER_CLOCKWISE) Collections.reverse(locations); Vec4 centerPoint = Vec4.computeAveragePoint(points); Vec4 surfaceNormal = globe.computeSurfaceNormalAtPoint(centerPoint); int numPoints = points.size(); float[] coords = new float[3 * numPoints]; for (int i = 0; i < numPoints; i++) { points.get(i).toFloatArray(coords, 3 * i, 3); } GeometryBuilder gb = new GeometryBuilder(); GeometryBuilder.IndexedTriangleArray tessellatedPoints = gb.tessellatePolygon(0, numPoints, coords, surfaceNormal); for (int i = 0; i < subdivisions; i++) { gb.subdivideIndexedTriangleArray(tessellatedPoints); } for (int i = 0; i < tessellatedPoints.getVertexCount(); i++) { Vec4 v = Vec4.fromFloatArray(tessellatedPoints.getVertices(), 3 * i, 3); tessellatedLocations.add(globe.computePositionFromPoint(v)); } }
@Override protected List<Vec4> computeMinimalGeometry(Globe globe, double verticalExaggeration) { double[] angles = this.computeAngles(); // Angles are equal, fall back to building a closed cylinder. if (angles == null) return super.computeMinimalGeometry(globe, verticalExaggeration); double[] radii = this.getRadii(); Matrix transform = this.computeTransform(globe, verticalExaggeration); GeometryBuilder gb = this.getGeometryBuilder(); int count = gb.getPartialDiskVertexCount(MINIMAL_GEOMETRY_SLICES, MINIMAL_GEOMETRY_LOOPS); int numCoords = 3 * count; float[] verts = new float[numCoords]; gb.makePartialDiskVertices( (float) radii[0], (float) radii[1], // Inner radius, outer radius. MINIMAL_GEOMETRY_SLICES, MINIMAL_GEOMETRY_LOOPS, // Slices, loops, (float) angles[0], (float) angles[2], // Start angle, sweep angle. verts); List<LatLon> locations = new ArrayList<LatLon>(); for (int i = 0; i < numCoords; i += 3) { Vec4 v = new Vec4(verts[i], verts[i + 1], verts[i + 2]); v = v.transformBy4(transform); locations.add(globe.computePositionFromPoint(v)); } ArrayList<Vec4> points = new ArrayList<Vec4>(); this.makeExtremePoints(globe, verticalExaggeration, locations, points); return points; }
private void makePartialCylinderTerrainConformant( DrawContext dc, int slices, int stacks, float[] verts, double[] altitudes, boolean[] terrainConformant, Vec4 referenceCenter) { Globe globe = dc.getGlobe(); Matrix transform = this.computeTransform(dc.getGlobe(), dc.getVerticalExaggeration()); for (int i = 0; i <= slices; i++) { int index = i * (stacks + 1); index = 3 * index; Vec4 vec = new Vec4(verts[index], verts[index + 1], verts[index + 2]); vec = vec.transformBy4(transform); Position p = globe.computePositionFromPoint(vec); for (int j = 0; j <= stacks; j++) { double elevation = altitudes[j]; if (terrainConformant[j]) elevation += this.computeElevationAt(dc, p.getLatitude(), p.getLongitude()); vec = globe.computePointFromPosition(p.getLatitude(), p.getLongitude(), elevation); index = j + i * (stacks + 1); index = 3 * index; verts[index] = (float) (vec.x - referenceCenter.x); verts[index + 1] = (float) (vec.y - referenceCenter.y); verts[index + 2] = (float) (vec.z - referenceCenter.z); } } }
private void makePartialDiskTerrainConformant( DrawContext dc, int numCoords, float[] verts, double altitude, boolean terrainConformant, Vec4 referenceCenter) { Globe globe = dc.getGlobe(); Matrix transform = this.computeTransform(dc.getGlobe(), dc.getVerticalExaggeration()); for (int i = 0; i < numCoords; i += 3) { Vec4 vec = new Vec4(verts[i], verts[i + 1], verts[i + 2]); vec = vec.transformBy4(transform); Position p = globe.computePositionFromPoint(vec); double elevation = altitude; if (terrainConformant) elevation += this.computeElevationAt(dc, p.getLatitude(), p.getLongitude()); vec = globe.computePointFromPosition(p.getLatitude(), p.getLongitude(), elevation); verts[i] = (float) (vec.x - referenceCenter.x); verts[i + 1] = (float) (vec.y - referenceCenter.y); verts[i + 2] = (float) (vec.z - referenceCenter.z); } }
protected boolean areShapesIntersecting(Airspace a1, Airspace a2) { if ((a1 instanceof SphereAirspace) && (a2 instanceof SphereAirspace)) { SphereAirspace s1 = (SphereAirspace) a1; SphereAirspace s2 = (SphereAirspace) a2; LatLon location1 = s1.getLocation(); LatLon location2 = s2.getLocation(); double altitude1 = s1.getAltitudes()[0]; double altitude2 = s2.getAltitudes()[0]; boolean terrainConforming1 = s1.isTerrainConforming()[0]; boolean terrainConforming2 = s2.isTerrainConforming()[0]; // We have to compute the 3D coordinates of the sphere's center ourselves here. Vec4 p1 = terrainConforming1 ? this.getSurfacePoint(location1, altitude1) : this.getPoint(location1, altitude1); Vec4 p2 = terrainConforming2 ? this.getSurfacePoint(location2, altitude2) : this.getPoint(location2, altitude2); double r1 = s1.getRadius(); double r2 = s2.getRadius(); double d = p1.distanceTo3(p2); return d <= (r1 + r2); } return false; }
/** * Performs one line of sight calculation between the reference position and a specified grid * position. * * @param gridPosition the grid position. * @throws InterruptedException if the operation is interrupted. */ protected void performIntersection(Position gridPosition) throws InterruptedException { // Intersect the line between this grid point and the selected position. Intersection[] intersections = this.terrain.intersect(this.referencePosition, gridPosition); if (intersections == null || intersections.length == 0) { // No intersection, so the line goes from the center to the grid point. this.sightLines.add(new Position[] {this.referencePosition, gridPosition}); return; } // Only the first intersection is shown. Vec4 iPoint = intersections[0].getIntersectionPoint(); Vec4 gPoint = terrain.getSurfacePoint( gridPosition.getLatitude(), gridPosition.getLongitude(), gridPosition.getAltitude()); // Check to see whether the intersection is beyond the grid point. if (iPoint.distanceTo3(this.referencePoint) >= gPoint.distanceTo3(this.referencePoint)) { // Intersection is beyond the grid point; the line goes from the center to the grid point. this.addSightLine(this.referencePosition, gridPosition); return; } // Compute the position corresponding to the intersection. Position iPosition = this.terrain.getGlobe().computePositionFromPoint(iPoint); // The sight line goes from the user-selected position to the intersection position. this.addSightLine(this.referencePosition, new Position(iPosition, 0)); // Keep track of the intersection positions. this.addIntersectionPosition(iPosition); this.updateProgress(); }
public static void main(String[] args) { BarycentricPlanarShape bc = new BarycentricQuadrilateral(i0, i1, i2, i3); for (Vec4 point : testPoints) { double[] w = bc.getBarycentricCoords(point); Vec4 p = bc.getPoint(w); double[] uv = bc.getBilinearCoords(w[1], w[2]); System.out.printf( "%s, %s: ( %f, %f, %f) : ( %f, %f), %s\n", point, p, w[0], w[1], w[2], uv[0], uv[1], p.equals(point) ? "true" : "false"); } // // BarycentricPlanarShape bc = new BarycentricQuadrilateral(new Vec4(4, 3, 0), new // Vec4(7, 1, 0), // new Vec4(10, 5, 0), new Vec4(7, 7, 0)); // // ArrayList<Vec4> points = makePoints(0, 0, 14, 10); // for (Vec4 point : points) // { // double[] w = bc.getBarycentricCoords(point); // Vec4 p = bc.getPoint(w); // double[] uv = bc.getBilinearCoords(w[1], w[2]); // // System.out.printf("%s, %s: ( %f, %f, %f) : ( %f, %f), %s\n", // point, p, w[0], w[1], w[2], uv[0], uv[1], p.equals(point) ? "true" : "false"); // } }
private void makeRadialWallTerrainConformant( DrawContext dc, int pillars, int stacks, float[] verts, double[] altitudes, boolean[] terrainConformant, Vec4 referenceCenter) { Globe globe = dc.getGlobe(); Matrix transform = this.computeTransform(dc.getGlobe(), dc.getVerticalExaggeration()); for (int p = 0; p <= pillars; p++) { int index = p; index = 3 * index; Vec4 vec = new Vec4(verts[index], verts[index + 1], verts[index + 2]); vec = vec.transformBy4(transform); Position pos = globe.computePositionFromPoint(vec); for (int s = 0; s <= stacks; s++) { double elevation = altitudes[s]; if (terrainConformant[s]) elevation += this.computeElevationAt(dc, pos.getLatitude(), pos.getLongitude()); vec = globe.computePointFromPosition(pos.getLatitude(), pos.getLongitude(), elevation); index = p + s * (pillars + 1); index = 3 * index; verts[index] = (float) (vec.x - referenceCenter.x); verts[index + 1] = (float) (vec.y - referenceCenter.y); verts[index + 2] = (float) (vec.z - referenceCenter.z); } } }
protected void requestTile(DrawContext dc, Tile tile) { Vec4 centroid = dc.getGlobe().computePointFromPosition(tile.getSector().getCentroid(), 0); if (this.getReferencePoint() != null) tile.setPriority(centroid.distanceTo3(this.getReferencePoint())); RequestTask task = new RequestTask(tile, this); this.getRequestQ().add(task); }
protected static boolean isNameVisible( DrawContext dc, PlaceNameService service, Position namePosition) { double elevation = dc.getVerticalExaggeration() * namePosition.getElevation(); Vec4 namePoint = dc.getGlobe() .computePointFromPosition( namePosition.getLatitude(), namePosition.getLongitude(), elevation); Vec4 eyeVec = dc.getView().getEyePoint(); double dist = eyeVec.distanceTo3(namePoint); return dist >= service.getMinDisplayDistance() && dist <= service.getMaxDisplayDistance(); }
private void makeCap( DrawContext dc, GeometryBuilder.IndexedTriangleArray ita, double altitude, boolean terrainConformant, int orientation, Matrix locationTransform, Vec4 referenceCenter, int indexPos, int[] indices, int vertexPos, float[] vertices, float[] normals) { GeometryBuilder gb = this.getGeometryBuilder(); Globe globe = dc.getGlobe(); int indexCount = ita.getIndexCount(); int vertexCount = ita.getVertexCount(); int[] locationIndices = ita.getIndices(); float[] locationVerts = ita.getVertices(); this.copyIndexArray( indexCount, (orientation == GeometryBuilder.INSIDE), locationIndices, vertexPos, indexPos, indices); for (int i = 0; i < vertexCount; i++) { int index = 3 * i; Vec4 vec = new Vec4(locationVerts[index], locationVerts[index + 1], locationVerts[index + 2]); vec = vec.transformBy4(locationTransform); Position pos = globe.computePositionFromPoint(vec); vec = this.computePointFromPosition( dc, pos.getLatitude(), pos.getLongitude(), altitude, terrainConformant); index = 3 * (vertexPos + i); vertices[index] = (float) (vec.x - referenceCenter.x); vertices[index + 1] = (float) (vec.y - referenceCenter.y); vertices[index + 2] = (float) (vec.z - referenceCenter.z); } gb.makeIndexedTriangleArrayNormals( indexPos, indexCount, indices, vertexPos, vertexCount, vertices, normals); }
private void makeSectionVertices( DrawContext dc, int locationPos, float[] locations, double[] altitude, boolean[] terrainConformant, int subdivisions, Matrix locationTransform, Vec4 referenceCenter, int vertexPos, float[] vertices) { GeometryBuilder gb = this.getGeometryBuilder(); int numPoints = gb.getSubdivisionPointsVertexCount(subdivisions); Globe globe = dc.getGlobe(); int index1 = 3 * locationPos; int index2 = 3 * (locationPos + 1); float[] locationVerts = new float[3 * numPoints]; gb.makeSubdivisionPoints( locations[index1], locations[index1 + 1], locations[index1 + 2], locations[index2], locations[index2 + 1], locations[index2 + 2], subdivisions, locationVerts); for (int i = 0; i < numPoints; i++) { int index = 3 * i; Vec4 vec = new Vec4(locationVerts[index], locationVerts[index + 1], locationVerts[index + 2]); vec = vec.transformBy4(locationTransform); Position pos = globe.computePositionFromPoint(vec); for (int j = 0; j < 2; j++) { vec = this.computePointFromPosition( dc, pos.getLatitude(), pos.getLongitude(), altitude[j], terrainConformant[j]); index = 2 * i + j; index = 3 * (vertexPos + index); vertices[index] = (float) (vec.x - referenceCenter.x); vertices[index + 1] = (float) (vec.y - referenceCenter.y); vertices[index + 2] = (float) (vec.z - referenceCenter.z); } } }
protected Extent computeExtent(Globe globe, double verticalExaggeration) { List<Vec4> points = this.computeMinimalGeometry(globe, verticalExaggeration); if (points == null || points.isEmpty()) return null; // Add a point at the center of this polygon to the points used to compute its extent. The // center point captures // the curvature of the globe when the polygon's minimal geometry only contain any points near // the polygon's // edges. Vec4 centerPoint = Vec4.computeAveragePoint(points); LatLon centerLocation = globe.computePositionFromPoint(centerPoint); this.makeExtremePoints(globe, verticalExaggeration, Arrays.asList(centerLocation), points); return Box.computeBoundingBox(points); }
/** * Compute points on either side of a line segment. This method requires a point on the line, and * either a next point, previous point, or both. * * @param point Center point about which to compute side points. * @param prev Previous point on the line. May be null if {@code next} is non-null. * @param next Next point on the line. May be null if {@code prev} is non-null. * @param leftPositions Left position will be added to this list. * @param rightPositions Right position will be added to this list. * @param halfWidth Distance from the center line to the left or right lines. * @param globe Current globe. */ protected void generateParallelPoints( Vec4 point, Vec4 prev, Vec4 next, List<Position> leftPositions, List<Position> rightPositions, double halfWidth, Globe globe) { if ((point == null) || (prev == null && next == null)) { String message = Logging.getMessage("nullValue.PointIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (leftPositions == null || rightPositions == null) { String message = Logging.getMessage("nullValue.PositionListIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (globe == null) { String message = Logging.getMessage("nullValue.GlobeIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } Vec4 offset; Vec4 normal = globe.computeSurfaceNormalAtPoint(point); // Compute vector in the direction backward along the line. Vec4 backward = (prev != null) ? prev.subtract3(point) : point.subtract3(next); // Compute a vector perpendicular to segment BC, and the globe normal vector. Vec4 perpendicular = backward.cross3(normal); double length; // If both next and previous points are supplied then calculate the angle that bisects the angle // current, next, prev. if (next != null && prev != null && !Vec4.areColinear(prev, point, next)) { // Compute vector in the forward direction. Vec4 forward = next.subtract3(point); // Calculate the vector that bisects angle ABC. offset = forward.normalize3().add3(backward.normalize3()); offset = offset.normalize3(); // Compute the scalar triple product of the vector BC, the normal vector, and the offset // vector to // determine if the offset points to the left or the right of the control line. double tripleProduct = perpendicular.dot3(offset); if (tripleProduct < 0) { offset = offset.multiply3(-1); } // Determine the length of the offset vector that will keep the left and right lines parallel // to the control // line. Angle theta = backward.angleBetween3(offset); if (!Angle.ZERO.equals(theta)) length = halfWidth / theta.sin(); else length = halfWidth; } else { offset = perpendicular.normalize3(); length = halfWidth; } offset = offset.multiply3(length); // Determine the left and right points by applying the offset. Vec4 ptRight = point.add3(offset); Vec4 ptLeft = point.subtract3(offset); // Convert cartesian points to geographic. Position posLeft = globe.computePositionFromPoint(ptLeft); Position posRight = globe.computePositionFromPoint(ptRight); leftPositions.add(posLeft); rightPositions.add(posRight); }
public static double[] invertBilinear(Vec4 U, Vec4 X, Vec4 Y, Vec4 Z, Vec4 W) { Vec4 s1 = W.subtract3(X); Vec4 s2 = Z.subtract3(Y); Vec4 UminX = U.subtract3(X); Vec4 UminY = U.subtract3(Y); Vec4 normal = Z.subtract3(X).cross3(W.subtract3(Y)); double A = s1.cross3(s2).dot3(normal); double B = s2.cross3(UminX).dot3(normal) - s1.cross3(UminY).dot3(normal); double C = UminX.cross3(UminY).dot3(normal); double descriminant = B * B - 4d * A * C; if (descriminant < 0) return null; descriminant = Math.sqrt(descriminant); double beta = B > 0 ? (-B - descriminant) / (2d * A) : 2d * C / (-B + descriminant); Vec4 Sbeta1 = Vec4.mix3(beta, X, W); Vec4 Sbeta2 = Vec4.mix3(beta, Y, Z); double alpha = U.subtract3(Sbeta1).dot3(Sbeta2.subtract3(Sbeta1)) / Sbeta2.subtract3(Sbeta1).dotSelf3(); return new double[] {alpha, beta}; }
/** * Compute the positions of the arrow head of the graphic's legs. * * @param dc Current draw context * @param base Position of the arrow's starting point. * @param tip Position of the arrow head tip. * @param arrowLength Length of the arrowhead as a fraction of the total line length. * @param arrowAngle Angle of the arrow head. * @return Positions required to draw the arrow head. */ protected List<Position> computeArrowheadPositions( DrawContext dc, Position base, Position tip, double arrowLength, Angle arrowAngle) { // Build a triangle to represent the arrowhead. The triangle is built from two vectors, one // parallel to the // segment, and one perpendicular to it. Globe globe = dc.getGlobe(); Vec4 ptA = globe.computePointFromPosition(base); Vec4 ptB = globe.computePointFromPosition(tip); // Compute parallel component Vec4 parallel = ptA.subtract3(ptB); Vec4 surfaceNormal = globe.computeSurfaceNormalAtPoint(ptB); // Compute perpendicular component Vec4 perpendicular = surfaceNormal.cross3(parallel); double finalArrowLength = arrowLength * parallel.getLength3(); double arrowHalfWidth = finalArrowLength * arrowAngle.tanHalfAngle(); perpendicular = perpendicular.normalize3().multiply3(arrowHalfWidth); parallel = parallel.normalize3().multiply3(finalArrowLength); // Compute geometry of direction arrow Vec4 vertex1 = ptB.add3(parallel).add3(perpendicular); Vec4 vertex2 = ptB.add3(parallel).subtract3(perpendicular); return TacticalGraphicUtil.asPositionList(globe, vertex1, vertex2, ptB); }
protected int computeCartesianPolygon( Globe globe, List<? extends LatLon> locations, List<Boolean> edgeFlags, Vec4[] points, Boolean[] edgeFlagArray, Matrix[] transform) { if (globe == null) { String message = Logging.getMessage("nullValue.GlobeIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (locations == null) { String message = "nullValue.LocationsIsNull"; Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (points == null) { String message = "nullValue.LocationsIsNull"; Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (points.length < (1 + locations.size())) { String message = Logging.getMessage( "generic.ArrayInvalidLength", "points.length < " + (1 + locations.size())); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (transform == null) { String message = "nullValue.TransformIsNull"; Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (transform.length < 1) { String message = Logging.getMessage("generic.ArrayInvalidLength", "transform.length < 1"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } // Allocate space to hold the list of locations and location vertices. int locationCount = locations.size(); // Compute the cartesian points for each location. for (int i = 0; i < locationCount; i++) { LatLon ll = locations.get(i); points[i] = globe.computePointFromPosition(ll.getLatitude(), ll.getLongitude(), 0.0); if (edgeFlagArray != null) edgeFlagArray[i] = (edgeFlags != null) ? edgeFlags.get(i) : true; } // Compute the average of the cartesian points. Vec4 centerPoint = Vec4.computeAveragePoint(Arrays.asList(points)); // Test whether the polygon is closed. If it is not closed, repeat the first vertex. if (!points[0].equals(points[locationCount - 1])) { points[locationCount] = points[0]; if (edgeFlagArray != null) edgeFlagArray[locationCount] = edgeFlagArray[0]; locationCount++; } // Compute a transform that will map the cartesian points to a local coordinate system centered // at the average // of the points and oriented with the globe surface. Position centerPos = globe.computePositionFromPoint(centerPoint); Matrix tx = globe.computeSurfaceOrientationAtPosition(centerPos); Matrix txInv = tx.getInverse(); // Map the cartesian points to a local coordinate space. for (int i = 0; i < locationCount; i++) { points[i] = points[i].transformBy4(txInv); } transform[0] = tx; return locationCount; }
/** * Determines whether this sector intersects the specified geographic line segment. The line * segment is specified by a begin location and an end location. The locations are are assumed to * be connected by a linear path in geographic space. This returns true if any location along that * linear path intersects this sector, including the begin and end locations. * * @param begin the line segment begin location. * @param end the line segment end location. * @return true <code>true</code> if this sector intersects the line segment, otherwise <code> * false</code>. * @throws IllegalArgumentException if either the begin location or the end location is null. */ public boolean intersectsSegment(LatLon begin, LatLon end) { if (begin == null) { throw new IllegalArgumentException("Begin Is Null"); } if (end == null) { throw new IllegalArgumentException("End Is Null"); } Vec4 segmentBegin = new Vec4(begin.getLongitude().degrees, begin.getLatitude().degrees, 0); Vec4 segmentEnd = new Vec4(end.getLongitude().degrees, end.getLatitude().degrees, 0); Vec4 tmp = segmentEnd.subtract3(segmentBegin); Vec4 segmentCenter = segmentBegin.add3(segmentEnd).divide3(2); Vec4 segmentDirection = tmp.normalize3(); double segmentExtent = tmp.getLength3() / 2.0; LatLon centroid = this.getCentroid(); Vec4 boxCenter = new Vec4(centroid.getLongitude().degrees, centroid.getLatitude().degrees, 0); double boxExtentX = this.getDeltaLonDegrees() / 2.0; double boxExtentY = this.getDeltaLatDegrees() / 2.0; Vec4 diff = segmentCenter.subtract3(boxCenter); if (Math.abs(diff.x) > (boxExtentX + segmentExtent * Math.abs(segmentDirection.x))) { return false; } if (Math.abs(diff.y) > (boxExtentY + segmentExtent * Math.abs(segmentDirection.y))) { return false; } //noinspection SuspiciousNameCombination Vec4 segmentPerp = new Vec4(segmentDirection.y, -segmentDirection.x, 0); return Math.abs(segmentPerp.dot3(diff)) <= (boxExtentX * Math.abs(segmentPerp.x) + boxExtentY * Math.abs(segmentPerp.y)); }
/** * Determine the positions that make up the arrowhead. * * @param dc Current draw context. * @param startPosition Position of the arrow's base. * @param endPosition Position of the arrow head tip. * @return Positions that define the arrowhead. */ protected List<Position> computeArrowheadPositions( DrawContext dc, Position startPosition, Position endPosition) { Globe globe = dc.getGlobe(); // Arrowhead looks like this: // _ // A\ | 1/2 width // ________B\ _| // Pt. 1 / // C/ // | | // Length Vec4 p1 = globe.computePointFromPosition(startPosition); Vec4 pB = globe.computePointFromPosition(endPosition); // Find vector in the direction of the arrow Vec4 vB1 = p1.subtract3(pB); double arrowLengthFraction = this.getArrowLength(); // Find the point at the base of the arrowhead Vec4 arrowBase = pB.add3(vB1.multiply3(arrowLengthFraction)); Vec4 normal = globe.computeSurfaceNormalAtPoint(arrowBase); // Compute the length of the arrowhead double arrowLength = vB1.getLength3() * arrowLengthFraction; double arrowHalfWidth = arrowLength * this.getArrowAngle().tanHalfAngle(); // Compute a vector perpendicular to the segment and the normal vector Vec4 perpendicular = vB1.cross3(normal); perpendicular = perpendicular.normalize3().multiply3(arrowHalfWidth); // Find points A and C Vec4 pA = arrowBase.add3(perpendicular); Vec4 pC = arrowBase.subtract3(perpendicular); return TacticalGraphicUtil.asPositionList(globe, pA, pB, pC); }