/* Copyright (C) 2001, 2006 United States Government as represented by the Administrator of the National Aeronautics and Space Administration. All Rights Reserved. */ package gov.nasa.worldwind.render; import gov.nasa.worldwind.*; import gov.nasa.worldwind.geom.*; import gov.nasa.worldwind.globes.*; import gov.nasa.worldwind.util.*; import javax.media.opengl.*; import java.awt.*; import java.util.*; /** * @author tag * @version $Id: Polyline.java 4571 2008-02-29 02:32:14Z tgaskins $ */ public class Polyline implements Renderable, Movable, Restorable { public final static int GREAT_CIRCLE = 0; public final static int LINEAR = 1; public final static int RHUMB_LINE = 2; public final static int LOXODROME = RHUMB_LINE; public final static int ANTIALIAS_DONT_CARE = GL.GL_DONT_CARE; public final static int ANTIALIAS_FASTEST = GL.GL_FASTEST; public final static int ANTIALIAS_NICEST = GL.GL_NICEST; private ArrayList<Position> positions; private Vec4 referenceCenterPoint; private Position referenceCenterPosition = Position.ZERO; private int antiAliasHint = GL.GL_FASTEST; private Color color = Color.WHITE; private double lineWidth = 1; private boolean filled = false; // makes it a polygon private boolean closed = false; // connect last point to first private boolean followTerrain = false; private double offset = 0; private double terrainConformance = 10; private int pathType = GREAT_CIRCLE; private ArrayList<ArrayList<Vec4>> currentSpans; private double length; private short stipplePattern = (short) 0xAAAA; private int stippleFactor = 0; private Globe globe; private int numSubsegments = 10; private boolean highlighted = false; private Color highlightColor = new Color(1f, 1f, 1f, 0.5f); public Polyline() { this.setPositions(null); } public Polyline(Iterable<Position> positions) { this.setPositions(positions); } public Polyline(Iterable<LatLon> positions, double elevation) { this.setPositions(positions, elevation); } private void reset() { if (this.currentSpans != null) this.currentSpans.clear(); this.currentSpans = null; } public Color getColor() { return color; } public void setColor(Color color) { if (color == null) { String msg = Logging.getMessage("nullValue.ColorIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } this.color = color; } public int getAntiAliasHint() { return antiAliasHint; } public void setAntiAliasHint(int hint) { if (!(hint == ANTIALIAS_DONT_CARE || hint == ANTIALIAS_FASTEST || hint == ANTIALIAS_NICEST)) { String msg = Logging.getMessage("generic.InvalidHint"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } this.antiAliasHint = hint; } public boolean isFilled() { return filled; } public void setFilled(boolean filled) { this.filled = filled; } public int getPathType() { return pathType; } /** * Sets the type of path to draw, one of {@link #GREAT_CIRCLE}, which draws each segment of the path as a great * circle, {@link #LINEAR}, which determines the intermediate positions between segments by interpolating the * segment endpoints, or {@link #RHUMB_LINE}, which draws each segment of the path as a line of constant heading. * * @param pathType the type of path to draw. */ public void setPathType(int pathType) { this.reset(); this.pathType = pathType; } public boolean isFollowTerrain() { return followTerrain; } /** * Indicates whether the path should follow the terrain's surface. If the value is <code>true</code>, the elevation * values in this path's positions are ignored and the path is drawn on the terrain surface. Otherwise the path is * drawn according to the elevations given in the path's positions. If following the terrain, the path may also have * an offset. See {@link #setOffset(double)}; * * @param followTerrain <code>true</code> to follow the terrain, otherwise <code>false</code>. */ public void setFollowTerrain(boolean followTerrain) { this.reset(); this.followTerrain = followTerrain; } public double getOffset() { return offset; } /** * Specifies an offset, in meters, to add to the path points when the path's follow-terrain attribute is true. See * {@link #setFollowTerrain(boolean)}. * * @param offset the path pffset in meters. */ public void setOffset(double offset) { this.reset(); this.offset = offset; } public double getTerrainConformance() { return terrainConformance; } /** * Specifies the precision to which the path follows the terrain when the follow-terrain attribute is true. The * conformance value indicates the approximate length of each sub-segment of the path as it's drawn, in pixels. * Lower values specify higher precision, but at the cost of performance. * * @param terrainConformance the path conformance in pixels. */ public void setTerrainConformance(double terrainConformance) { this.terrainConformance = terrainConformance; } public double getLineWidth() { return lineWidth; } public void setLineWidth(double lineWidth) { this.lineWidth = lineWidth; } /** * Returns the length of the line as drawn. If the path follows the terrain, the length returned is the distance one * would travel if on the surface. If the path does not follow the terrain, the length returned is the distance * along the full length of the path at the path's elevations and current path type. * * @return the path's length in meters. */ public double getLength() { return length; } public short getStipplePattern() { return stipplePattern; } /** * Sets the stipple pattern for specifying line types other than solid. See the OpenGL specification or programming * guides for a description of this parameter. Stipple is also affected by the path's stipple factor, {@link * #setStippleFactor(int)}. * * @param stipplePattern the stipple pattern. */ public void setStipplePattern(short stipplePattern) { this.stipplePattern = stipplePattern; } public int getStippleFactor() { return stippleFactor; } /** * Sets the stipple factor for specifying line types other than solid. See the OpenGL specification or programming * guides for a description of this parameter. Stipple is also affected by the path's stipple pattern, {@link * #setStipplePattern(short)}. * * @param stippleFactor the stipple factor. */ public void setStippleFactor(int stippleFactor) { this.stippleFactor = stippleFactor; } public int getNumSubsegments() { return numSubsegments; } /** * Specifies the number of intermediate segments to draw for each segment between positions. The end points of the * intermediate segments are calculated according to the current path type and follow-terrain setting. * * @param numSubsegments the number of intermediate subsegments. */ public void setNumSubsegments(int numSubsegments) { this.reset(); this.numSubsegments = numSubsegments; } public boolean isHighlighted() { return highlighted; } public void setHighlighted(boolean highlighted) { this.highlighted = highlighted; } public Color getHighlightColor() { return this.highlightColor; } public void setHighlightColor(Color highlightColor) { if (highlightColor == null) { String message = Logging.getMessage("nullValue.ColorIsNull"); Logging.logger().severe(message); throw new IllegalStateException(message); } this.highlightColor = highlightColor; } /** * Specifies the path's positions. * * @param inPositions the path positions. */ public void setPositions(Iterable<Position> inPositions) { this.reset(); this.positions = new ArrayList<Position>(); if (inPositions != null) { for (Position position : inPositions) { this.positions.add(position); } } if ((this.filled && this.positions.size() < 3)) { String msg = Logging.getMessage("generic.InsufficientPositions"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } } /** * Sets the paths positions as latitude and longitude values at a constant altitude. * * @param inPositions the latitudes and longitudes of the positions. * @param elevation the elevation to assign each position. */ public void setPositions(Iterable<LatLon> inPositions, double elevation) { this.reset(); this.positions = new ArrayList<Position>(); if (inPositions != null) { for (LatLon position : inPositions) { this.positions.add(new Position(position, elevation)); } } if (this.filled && this.positions.size() < 3) { String msg = Logging.getMessage("generic.InsufficientPositions"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } } public Iterable<Position> getPositions() { return this.positions; } public boolean isClosed() { return closed; } public void setClosed(boolean closed) { this.closed = closed; } public void render(DrawContext dc) { if (dc == null) { String message = Logging.getMessage("nullValue.DrawContextIsNull"); Logging.logger().severe(message); throw new IllegalStateException(message); } this.globe = dc.getGlobe(); if (this.positions.size() < 2) return; if (this.currentSpans == null || this.followTerrain) // vertices computed every frame to follow terrain changes { // Reference center must be computed prior to computing vertices. this.computeReferenceCenter(dc); this.makeVertices(dc); } if (this.currentSpans == null || this.currentSpans.size() < 1) { return; } GL gl = dc.getGL(); int attrBits = GL.GL_HINT_BIT | GL.GL_CURRENT_BIT | GL.GL_LINE_BIT; if (!dc.isPickingMode()) { if (this.color.getAlpha() != 255) attrBits |= GL.GL_COLOR_BUFFER_BIT; } gl.glPushAttrib(attrBits); dc.getView().pushReferenceCenter(dc, this.referenceCenterPoint); try { if (!dc.isPickingMode()) { if (this.color.getAlpha() != 255) { gl.glEnable(GL.GL_BLEND); gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA); } dc.getGL().glColor4ub((byte) this.color.getRed(), (byte) this.color.getGreen(), (byte) this.color.getBlue(), (byte) this.color.getAlpha()); } if (this.stippleFactor > 0) { gl.glEnable(GL.GL_LINE_STIPPLE); gl.glLineStipple(this.stippleFactor, this.stipplePattern); } else { gl.glDisable(GL.GL_LINE_STIPPLE); } int hintAttr = GL.GL_LINE_SMOOTH_HINT; if (this.filled) hintAttr = GL.GL_POLYGON_SMOOTH_HINT; gl.glHint(hintAttr, this.antiAliasHint); int primType = GL.GL_LINE_STRIP; if (this.filled) primType = GL.GL_POLYGON; if (dc.isPickingMode()) gl.glLineWidth((float) this.lineWidth + 8); else gl.glLineWidth((float) this.lineWidth); if (this.followTerrain) this.pushOffest(dc); for (ArrayList<Vec4> span : this.currentSpans) { if (span == null) continue; // Since segements can very often be very short -- two vertices -- use explicit rendering. The // overhead of batched rendering, e.g., gl.glDrawArrays, is too high because it requires copying // the vertices into a DoubleBuffer, and DoubleBuffer creation and access performs relatively poorly. gl.glBegin(primType); for (Vec4 p : span) { gl.glVertex3d(p.x, p.y, p.z); } gl.glEnd(); } if (this.highlighted) { if (!dc.isPickingMode()) { if (this.highlightColor.getAlpha() != 255) { gl.glEnable(GL.GL_BLEND); gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA); } dc.getGL().glColor4ub((byte) this.highlightColor.getRed(), (byte) this.highlightColor.getGreen(), (byte) this.highlightColor.getBlue(), (byte) this.highlightColor.getAlpha()); gl.glLineWidth((float) this.lineWidth + 2); for (ArrayList<Vec4> span : this.currentSpans) { if (span == null) continue; gl.glBegin(primType); for (Vec4 p : span) { gl.glVertex3d(p.x, p.y, p.z); } gl.glEnd(); } } } if (this.followTerrain) this.popOffest(dc); } finally { gl.glPopAttrib(); dc.getView().popReferenceCenter(dc); } } private void pushOffest(DrawContext dc) { // Modify the projection transform to shift the depth values slightly toward the camera in order to // ensure the lines are selected during depth buffering. GL gl = dc.getGL(); float[] pm = new float[16]; gl.glGetFloatv(GL.GL_PROJECTION_MATRIX, pm, 0); pm[10] *= 0.99; // TODO: See Lengyel 2 ed. Section 9.1.2 to compute optimal/minimal offset gl.glPushAttrib(GL.GL_TRANSFORM_BIT); gl.glMatrixMode(GL.GL_PROJECTION); gl.glPushMatrix(); gl.glLoadMatrixf(pm, 0); } private void popOffest(DrawContext dc) { GL gl = dc.getGL(); gl.glMatrixMode(GL.GL_PROJECTION); gl.glPopMatrix(); gl.glPopAttrib(); } protected void makeVertices(DrawContext dc) { if (this.currentSpans == null) this.currentSpans = new ArrayList<ArrayList<Vec4>>(); else this.currentSpans.clear(); this.length = 0; if (this.positions.size() < 1) return; Position posA = this.positions.get(0); Vec4 ptA = this.computePoint(dc, posA, true); for (int i = 1; i <= this.positions.size(); i++) { Position posB; if (i < this.positions.size()) posB = this.positions.get(i); else if (this.closed) posB = this.positions.get(0); else break; Vec4 ptB = this.computePoint(dc, posB, true); if (this.followTerrain && !this.isSegmentVisible(dc, posA, posB, ptA, ptB)) { posA = posB; ptA = ptB; continue; } ArrayList<Vec4> span; span = this.makeSegment(dc, posA, posB, ptA, ptB); if (span != null) this.addSpan(span); posA = posB; ptA = ptB; } } private void addSpan(ArrayList<Vec4> span) { if (span != null && span.size() > 0) this.currentSpans.add(span); } private boolean isSegmentVisible(DrawContext dc, Position posA, Position posB, Vec4 ptA, Vec4 ptB) { Frustum f = dc.getView().getFrustumInModelCoordinates(); if (f.contains(ptA)) return true; if (f.contains(ptB)) return true; if (ptA.equals(ptB)) return false; Position posC = Position.interpolate(0.5, posA, posB); Vec4 ptC = this.computePoint(dc, posC, true); if (f.contains(ptC)) return true; // TODO: Find a more efficient bounding geometry for this frustum intersection test. double r = Line.distanceToSegment(ptA, ptB, ptC); Cylinder cyl = new Cylinder(ptA, ptB, r == 0 ? 1 : r); return cyl.intersects(dc.getView().getFrustumInModelCoordinates()); } private Vec4 computePoint(DrawContext dc, Position pos, boolean applyOffset) { if (this.followTerrain) { double height = !applyOffset ? 0 : this.offset; return this.computeTerrainPoint(dc, pos.getLatitude(), pos.getLongitude(), height); } else { double height = pos.getElevation() + (applyOffset ? this.offset : 0); return dc.getGlobe().computePointFromPosition(pos.getLatitude(), pos.getLongitude(), height); } } private double computeSegmentLength(DrawContext dc, Position posA, Position posB) { LatLon llA = new LatLon(posA.getLatitude(), posA.getLongitude()); LatLon llB = new LatLon(posB.getLatitude(), posB.getLongitude()); Angle ang = LatLon.greatCircleDistance(llA, llB); if (this.followTerrain) { return ang.radians * (dc.getGlobe().getRadius() + this.offset); } else { double height = this.offset + 0.5 * (posA.getElevation() + posB.getElevation()); return ang.radians * (dc.getGlobe().getRadius() + height); } } private ArrayList<Vec4> makeSegment(DrawContext dc, Position posA, Position posB, Vec4 ptA, Vec4 ptB) { ArrayList<Vec4> span = null; double arcLength = this.computeSegmentLength(dc, posA, posB); if (arcLength <= 0) // points differing only in altitude { span = this.addPointToSpan(ptA, span); if (!ptA.equals(ptB)) span = this.addPointToSpan(ptB, span); return span; } // Variables for great circle and rhumb computation. Angle segmentAzimuth = null; Angle segmentDistance = null; for (double s = 0, p = 0; s < 1;) { if (this.followTerrain) p += this.terrainConformance * dc.getView().computePixelSizeAtDistance( ptA.distanceTo3(dc.getView().getEyePoint())); else p += arcLength / this.numSubsegments; s = p / arcLength; Position pos; if (s >= 1) { pos = posB; } else if (this.pathType == LINEAR) { pos = Position.interpolate(s, posA, posB); } else if (this.pathType == RHUMB_LINE) // or LOXODROME { if (segmentAzimuth == null) { segmentAzimuth = LatLon.rhumbAzimuth(posA.getLatLon(), posB.getLatLon()); segmentDistance = LatLon.rhumbDistance(posA.getLatLon(), posB.getLatLon()); } Angle distance = Angle.fromRadians(s * segmentDistance.radians); LatLon latLon = LatLon.rhumbEndPosition(posA.getLatLon(), segmentAzimuth, distance); pos = new Position(latLon, (1 - s) * posA.getElevation() + s * posB.getElevation()); } else // GREAT_CIRCLE { if (segmentAzimuth == null) { segmentAzimuth = LatLon.greatCircleAzimuth(posA.getLatLon(), posB.getLatLon()); segmentDistance = LatLon.greatCircleDistance(posA.getLatLon(), posB.getLatLon()); } Angle distance = Angle.fromRadians(s * segmentDistance.radians); LatLon latLon = LatLon.greatCircleEndPosition(posA.getLatLon(), segmentAzimuth, distance); pos = new Position(latLon, (1 - s) * posA.getElevation() + s * posB.getElevation()); } ptB = this.computePoint(dc, pos, true); span = this.clipAndAdd(dc, ptA, ptB, span); this.length += ptA.distanceTo3(ptB); ptA = ptB; } return span; } @SuppressWarnings({"UnusedDeclaration"}) private ArrayList<Vec4> clipAndAdd(DrawContext dc, Vec4 ptA, Vec4 ptB, ArrayList<Vec4> span) { // Line clipping appears to be useful only for long lines with few segments. It's costly otherwise. // TODO: Investigate trade-off of line clipping. // if (Line.clipToFrustum(ptA, ptB, dc.getView().getFrustumInModelCoordinates()) == null) // { // if (span != null) // { // this.addSpan(span); // span = null; // } // return span; // } if (span == null) span = this.addPointToSpan(ptA, span); return this.addPointToSpan(ptB, span); } private ArrayList<Vec4> addPointToSpan(Vec4 p, ArrayList<Vec4> span) { if (span == null) span = new ArrayList<Vec4>(); span.add(p.subtract3(this.referenceCenterPoint)); return span; } private void computeReferenceCenter(DrawContext dc) { if (this.positions.size() < 1) return; if (this.positions.size() < 3) this.referenceCenterPosition = this.positions.get(0); else this.referenceCenterPosition = this.positions.get(this.positions.size() / 2); this.referenceCenterPoint = this.computeTerrainPoint(dc, this.referenceCenterPosition.getLatitude(), this.referenceCenterPosition.getLongitude(), this.offset); } public Position getReferencePosition() { return this.referenceCenterPosition; } private Vec4 computeTerrainPoint(DrawContext dc, Angle lat, Angle lon, double offset) { Vec4 p = dc.getSurfaceGeometry().getSurfacePoint(lat, lon, offset); if (p == null) { p = dc.getGlobe().computePointFromPosition(lat, lon, offset + dc.getGlobe().getElevation(lat, lon) * dc.getVerticalExaggeration()); } return p; } public void move(Position delta) { if (delta == null) { String msg = Logging.getMessage("nullValue.PositionIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } this.moveTo(this.getReferencePosition().add(delta)); } public void moveTo(Position position) { if (position == null) { String msg = Logging.getMessage("nullValue.PositionIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } this.reset(); if (this.positions.size() < 1) return; Vec4 origRef = this.referenceCenterPoint; Vec4 newRef = this.globe.computePointFromPosition(position); Angle distance = LatLon.greatCircleDistance(this.referenceCenterPosition.getLatLon(), position.getLatLon()); Vec4 axis = origRef.cross3(newRef).normalize3(); Quaternion q = Quaternion.fromAxisAngle(distance, axis); for (int i = 0; i < this.positions.size(); i++) { Position pos = this.positions.get(i); Vec4 p = this.globe.computePointFromPosition(pos); p = p.transformBy3(q); pos = this.globe.computePositionFromPoint(p); this.positions.set(i, pos); } } /** * Returns an XML state document String describing the public attributes of this Polyline. * * @return XML state document string describing this Polyline. */ public String getRestorableState() { RestorableSupport restorableSupport = RestorableSupport.newRestorableSupport(); // Creating a new RestorableSupport failed. RestorableSupport logged the problem, so just return null. if (restorableSupport == null) return null; if (this.color != null) { String encodedColor = RestorableSupport.encodeColor(this.color); if (encodedColor != null) restorableSupport.addStateValueAsString("color", encodedColor); } if (this.highlightColor != null) { String encodedColor = RestorableSupport.encodeColor(this.highlightColor); if (encodedColor != null) restorableSupport.addStateValueAsString("highlightColor", encodedColor); } if (this.positions != null) { // Create the base "positions" state object. RestorableSupport.StateObject positionsStateObj = restorableSupport.addStateObject("positions"); if (positionsStateObj != null) { for (Position p : this.positions) { // Save each position only if all parts (latitude, longitude, and elevation) can be // saved. We will not save a partial iconPosition (for example, just the elevation). if (p != null && p.getLatitude() != null && p.getLongitude() != null) { // Create a nested "position" element underneath the base "positions". RestorableSupport.StateObject pStateObj = restorableSupport.addStateObject(positionsStateObj, "position"); if (pStateObj != null) { restorableSupport.addStateValueAsDouble(pStateObj, "latitudeDegrees", p.getLatitude().degrees); restorableSupport.addStateValueAsDouble(pStateObj, "longitudeDegrees", p.getLongitude().degrees); restorableSupport.addStateValueAsDouble(pStateObj, "elevation", p.getElevation()); } } } } } restorableSupport.addStateValueAsInteger("antiAliasHint", this.antiAliasHint); restorableSupport.addStateValueAsBoolean("filled", this.filled); restorableSupport.addStateValueAsBoolean("closed", this.closed); restorableSupport.addStateValueAsBoolean("highlighted", this.highlighted); restorableSupport.addStateValueAsInteger("pathType", this.pathType); restorableSupport.addStateValueAsBoolean("followTerrain", this.followTerrain); restorableSupport.addStateValueAsDouble("offset", this.offset); restorableSupport.addStateValueAsDouble("terrainConformance", this.terrainConformance); restorableSupport.addStateValueAsDouble("lineWidth", this.lineWidth); restorableSupport.addStateValueAsInteger("stipplePattern", this.stipplePattern); restorableSupport.addStateValueAsInteger("stippleFactor", this.stippleFactor); restorableSupport.addStateValueAsInteger("numSubsegments", this.numSubsegments); return restorableSupport.getStateAsXml(); } /** * Restores publicly settable attribute values found in the specified XML state document String. The * document specified by <code>stateInXml</code> must be a well formed XML document String, or this will throw an * IllegalArgumentException. Unknown structures in <code>stateInXml</code> are benign, because they will * simply be ignored. * * @param stateInXml an XML document String describing a Polyline. * @throws IllegalArgumentException If <code>stateInXml</code> is null, or if <code>stateInXml</code> is not * a well formed XML document String. */ public void restoreState(String stateInXml) { if (stateInXml == null) { String message = Logging.getMessage("nullValue.StringIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } RestorableSupport restorableSupport; try { restorableSupport = RestorableSupport.parse(stateInXml); } catch (Exception e) { // Parsing the document specified by stateInXml failed. String message = Logging.getMessage("generic.ExceptionAttemptingToParseStateXml", stateInXml); Logging.logger().severe(message); throw new IllegalArgumentException(message, e); } String colorState = restorableSupport.getStateValueAsString("color"); if (colorState != null) { Color color = RestorableSupport.decodeColor(colorState); if (color != null) setColor(color); } colorState = restorableSupport.getStateValueAsString("highlightColor"); if (colorState != null) { Color color = RestorableSupport.decodeColor(colorState); if (color != null) setHighlightColor(color); } // Get the base "positions" state object. RestorableSupport.StateObject positionsStateObj = restorableSupport.getStateObject("positions"); if (positionsStateObj != null) { ArrayList<Position> newPositions = new ArrayList<Position>(); // Get the nested "position" states beneath the base "positions". RestorableSupport.StateObject[] positionStateArray = restorableSupport.getAllStateObjects(positionsStateObj, "position"); if (positionStateArray != null && positionStateArray.length != 0) { for (RestorableSupport.StateObject pStateObj : positionStateArray) { if (pStateObj != null) { // Restore each position only if all parts are available. // We will not restore a partial position (for example, just the elevation). Double latitudeState = restorableSupport.getStateValueAsDouble(pStateObj, "latitudeDegrees"); Double longitudeState = restorableSupport.getStateValueAsDouble(pStateObj, "longitudeDegrees"); Double elevationState = restorableSupport.getStateValueAsDouble(pStateObj, "elevation"); if (latitudeState != null && longitudeState != null && elevationState != null) newPositions.add(Position.fromDegrees(latitudeState, longitudeState, elevationState)); } } } // Even if there are no actual positions specified, we set positions as an empty list. // An empty set of positions is still a valid state. setPositions(newPositions); } Integer antiAliasHintState = restorableSupport.getStateValueAsInteger("antiAliasHint"); if (antiAliasHintState != null) setAntiAliasHint(antiAliasHintState); Boolean isFilledState = restorableSupport.getStateValueAsBoolean("filled"); if (isFilledState != null) setFilled(isFilledState); Boolean isClosedState = restorableSupport.getStateValueAsBoolean("closed"); if (isClosedState != null) setClosed(isClosedState); Boolean isHighlightedState = restorableSupport.getStateValueAsBoolean("highlighted"); if (isHighlightedState != null) setHighlighted(isHighlightedState); Integer pathTypeState = restorableSupport.getStateValueAsInteger("pathType"); if (pathTypeState != null) setPathType(pathTypeState); Boolean isFollowTerrainState = restorableSupport.getStateValueAsBoolean("followTerrain"); if (isFollowTerrainState != null) setFollowTerrain(isFollowTerrainState); Double offsetState = restorableSupport.getStateValueAsDouble("offset"); if (offsetState != null) setOffset(offsetState); Double terrainConformanceState = restorableSupport.getStateValueAsDouble("terrainConformance"); if (terrainConformanceState != null) setTerrainConformance(terrainConformanceState); Double lineWidthState = restorableSupport.getStateValueAsDouble("lineWidth"); if (lineWidthState != null) setLineWidth(lineWidthState); Integer stipplePatternState = restorableSupport.getStateValueAsInteger("stipplePattern"); if (stipplePatternState != null) setStipplePattern(stipplePatternState.shortValue()); Integer stippleFactorState = restorableSupport.getStateValueAsInteger("stippleFactor"); if (stippleFactorState != null) setStippleFactor(stippleFactorState); Integer numSubsegmentsState = restorableSupport.getStateValueAsInteger("numSubsegments"); if (numSubsegmentsState != null) setNumSubsegments(numSubsegmentsState); } }