/******************************************************************************* * Copyright 2014 Geoscience Australia * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package au.gov.ga.earthsci.worldwind.common.view.orbit; import gov.nasa.worldwind.View; import gov.nasa.worldwind.WWObjectImpl; import gov.nasa.worldwind.animation.Animator; import gov.nasa.worldwind.awt.ViewInputHandler; import gov.nasa.worldwind.geom.Angle; import gov.nasa.worldwind.geom.Frustum; import gov.nasa.worldwind.geom.Line; import gov.nasa.worldwind.geom.Matrix; import gov.nasa.worldwind.geom.Position; import gov.nasa.worldwind.geom.Vec4; import gov.nasa.worldwind.globes.Globe; import gov.nasa.worldwind.render.DrawContext; import gov.nasa.worldwind.util.Logging; import gov.nasa.worldwind.util.OGLStackHandler; import gov.nasa.worldwind.util.RestorableSupport; import gov.nasa.worldwind.view.BasicView; import gov.nasa.worldwind.view.ViewUtil; import gov.nasa.worldwind.view.firstperson.BasicFlyView; import gov.nasa.worldwind.view.orbit.BasicOrbitView; import javax.media.opengl.GL2; /** * Abstract implementation of the {@link View} interface. * <p/> * Largely copied from the {@link BasicView} class, but with the * heading/pitch/roll storage removed. Concrete subclasses can choose how to * store view state, as there are many different ways that this state can be * stored. Some examples: * <ul> * <li>center/heading/pitch/roll/zoom (like the {@link BasicOrbitView})</li> * <li>eye/heading/pitch/roll (like the {@link BasicFlyView})</li> * <li>center/rotation/distance (representing the rotation as a quaternion would * result in more rotation freedom)</li> * <li>center/eye/roll</li> * <li>eye/rotation</li> * </ul> * You may also choose to represent your center/eye points as {@link Position}s * (geographic) or {@link Vec4}s (cartesian), depending on your movement type. * * @author Michael de Hoog (michael.dehoog@ga.gov.au) */ @SuppressWarnings("nls") public abstract class AbstractView extends WWObjectImpl implements View { /** The field of view in degrees. */ protected Angle fieldOfView = Angle.fromDegrees(45); // Provide reasonable default values for the near and far clip distances. By default, BasicView automatically // updates these values each frame based on the current eye position relative to the surface. These default values // are provided for two reasons: // * The view can provide a reasonable value to the application until the first frame. // * Subclass implementations which may override the automatic update of clipping plane distances have reasonable // default values to fall back on. protected double nearClipDistance = MINIMUM_NEAR_DISTANCE; protected double farClipDistance = MINIMUM_FAR_DISTANCE; protected Matrix modelview = Matrix.IDENTITY; protected Matrix modelviewInv = Matrix.IDENTITY; protected Matrix projection = Matrix.IDENTITY; protected java.awt.Rectangle viewport = new java.awt.Rectangle(); protected Frustum frustum = new Frustum(); protected Frustum lastFrustumInModelCoords = null; protected DrawContext dc; protected boolean detectCollisions = true; protected boolean hadCollisions; protected ViewInputHandler viewInputHandler; protected Globe globe; protected double horizonDistance; /** * Identifier for the modelview matrix state. This number is incremented * when one of the fields that affects the modelview matrix is set. */ protected long viewStateID; // TODO: make configurable protected static final double MINIMUM_NEAR_DISTANCE = 2; protected static final double MINIMUM_FAR_DISTANCE = 100; protected static final double COLLISION_THRESHOLD = 10; protected static final int COLLISION_NUM_ITERATIONS = 4; @Override public Globe getGlobe() { return this.globe; } /** * Set the globe associated with this view. Note that the globe is reset * each frame. * * @param globe * New globe. */ public void setGlobe(Globe globe) { this.globe = globe; } public DrawContext getDC() { return (this.dc); } @Override public ViewInputHandler getViewInputHandler() { return viewInputHandler; } public void setViewInputHandler(ViewInputHandler viewInputHandler) { this.viewInputHandler = viewInputHandler; } public boolean isDetectCollisions() { return this.detectCollisions; } public void setDetectCollisions(boolean detectCollisions) { this.detectCollisions = detectCollisions; } public boolean hadCollisions() { boolean result = this.hadCollisions; this.hadCollisions = false; return result; } @Override public void copyViewState(View view) { this.globe = view.getGlobe(); Vec4 center = view.getCenterPoint(); if (center == null) { Vec4 eyePoint = view.getCurrentEyePoint(); center = eyePoint.add3(view.getForwardVector()); } setOrientation(view.getCurrentEyePosition(), globe.computePositionFromPoint(center)); } @Override public void apply(DrawContext dc) { if (dc == null) { String message = Logging.getMessage("nullValue.DrawContextIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (dc.getGL() == null) { String message = Logging.getMessage("nullValue.DrawingContextGLIsNull"); Logging.logger().severe(message); throw new IllegalStateException(message); } if (dc.getGlobe() == null) { String message = Logging.getMessage("layers.AbstractLayer.NoGlobeSpecifiedInDrawingContext"); Logging.logger().severe(message); throw new IllegalStateException(message); } // Update DrawContext and Globe references. this.dc = dc; this.globe = this.dc.getGlobe(); if (this.viewInputHandler != null) { this.viewInputHandler.apply(); } doApply(dc); if (this.viewInputHandler != null) { this.viewInputHandler.viewApplied(); } } protected abstract void doApply(DrawContext dc); @Override public void stopMovement() { this.firePropertyChange(VIEW_STOPPED, null, this); } @Override public java.awt.Rectangle getViewport() { // java.awt.Rectangle is mutable, so we defensively copy the viewport. return new java.awt.Rectangle(this.viewport); } @Override public Frustum getFrustum() { return this.frustum; } @Override public Frustum getFrustumInModelCoordinates() { if (this.lastFrustumInModelCoords == null) { Matrix modelviewTranspose = this.modelview.getTranspose(); if (modelviewTranspose != null) { this.lastFrustumInModelCoords = this.frustum.transformBy(modelviewTranspose); } else { this.lastFrustumInModelCoords = this.frustum; } } return this.lastFrustumInModelCoords; } @Override public void setFieldOfView(Angle fieldOfView) { if (fieldOfView == null) { String message = Logging.getMessage("nullValue.AngleIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.fieldOfView = fieldOfView; } @Override public double getNearClipDistance() { return this.nearClipDistance; } protected void setNearClipDistance(double clipDistance) { this.nearClipDistance = clipDistance; } @Override public double getFarClipDistance() { return this.farClipDistance; } protected void setFarClipDistance(double clipDistance) { this.farClipDistance = clipDistance; } @Override public Matrix getModelviewMatrix() { return this.modelview; } /** {@inheritDoc} */ @Override public long getViewStateID() { return this.viewStateID; } @Override public Angle getFieldOfView() { return this.fieldOfView; } @Override public Vec4 project(Vec4 modelPoint) { if (modelPoint == null) { String message = Logging.getMessage("nullValue.Vec4IsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } return this.project(modelPoint, this.modelview, this.projection, this.viewport); } @Override public Vec4 unProject(Vec4 windowPoint) { if (windowPoint == null) { String message = Logging.getMessage("nullValue.Vec4IsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } return unProject(windowPoint, this.modelview, this.projection, this.viewport); } @Override public void stopAnimations() { viewInputHandler.stopAnimators(); } @Override public boolean isAnimating() { return viewInputHandler.isAnimating(); } @Override public void goTo(Position position, double distance) { viewInputHandler.goTo(position, distance); } @Override public Line computeRayFromScreenPoint(double x, double y) { return ViewUtil.computeRayFromScreenPoint(this, x, y, this.modelview, this.projection, this.viewport); } @Override public Position computePositionFromScreenPoint(double x, double y) { if (this.globe != null) { Line ray = computeRayFromScreenPoint(x, y); if (ray != null) { return this.globe.getIntersectionPosition(ray); } } return null; } @Override public double computePixelSizeAtDistance(double distance) { return ViewUtil.computePixelSizeAtDistance(distance, this.fieldOfView, this.viewport); } protected Position computeEyePositionFromModelview() { if (this.globe != null) { Vec4 eyePoint = Vec4.UNIT_W.transformBy4(this.modelviewInv); return this.globe.computePositionFromPoint(eyePoint); } return Position.ZERO; } @Override public double getHorizonDistance() { return this.horizonDistance; } protected double computeHorizonDistance() { return this.computeHorizonDistance(computeEyePositionFromModelview()); } protected double computeHorizonDistance(Position eyePosition) { if (this.globe != null && eyePosition != null) { double elevation = eyePosition.getElevation(); double elevationAboveSurface = ViewUtil.computeElevationAboveSurface(this.dc, eyePosition); return ViewUtil.computeHorizonDistance(this.globe, Math.max(elevation, elevationAboveSurface)); } return 0; } protected double computeNearClipDistance() { return computeNearDistance(getCurrentEyePosition()); } protected double computeFarClipDistance() { return computeFarDistance(getCurrentEyePosition()); } protected double computeNearDistance(Position eyePosition) { double near = 0; if (eyePosition != null && this.dc != null) { double elevation = ViewUtil.computeElevationAboveSurface(this.dc, eyePosition); double tanHalfFov = this.fieldOfView.tanHalfAngle(); near = elevation / (2 * Math.sqrt(2 * tanHalfFov * tanHalfFov + 1)); } return near < MINIMUM_NEAR_DISTANCE ? MINIMUM_NEAR_DISTANCE : near; } protected double computeFarDistance(Position eyePosition) { double far = 0; if (eyePosition != null) { far = computeHorizonDistance(eyePosition); } return far < MINIMUM_FAR_DISTANCE ? MINIMUM_FAR_DISTANCE : far; } @Override public Matrix getProjectionMatrix() { return this.projection; } @Override public String getRestorableState() { RestorableSupport rs = RestorableSupport.newRestorableSupport(); // Creating a new RestorableSupport failed. RestorableSupport logged the problem, so just return null. if (rs == null) { return null; } this.doGetRestorableState(rs, null); return rs.getStateAsXml(); } @Override public void restoreState(String stateInXml) { if (stateInXml == null) { String message = Logging.getMessage("nullValue.StringIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } RestorableSupport rs; try { rs = 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); } this.doRestoreState(rs, null); } /** * Update the modelview state identifier. This method should be called * whenever one of the fields that affects the modelview matrix is changed. */ protected void updateModelViewStateID() { this.viewStateID++; } //**************************************************************// //******************** Restorable State ***********************// //**************************************************************// protected void doGetRestorableState(RestorableSupport rs, RestorableSupport.StateObject context) { this.getViewPropertyLimits().getRestorableState(rs, rs.addStateObject(context, "viewPropertyLimits")); rs.addStateValueAsBoolean(context, "detectCollisions", this.isDetectCollisions()); if (this.getFieldOfView() != null) { rs.addStateValueAsDouble(context, "fieldOfView", this.getFieldOfView().getDegrees()); } rs.addStateValueAsDouble(context, "nearClipDistance", this.getNearClipDistance()); rs.addStateValueAsDouble(context, "farClipDistance", this.getFarClipDistance()); if (this.getEyePosition() != null) { rs.addStateValueAsPosition(context, "eyePosition", this.getEyePosition()); } if (this.getHeading() != null) { rs.addStateValueAsDouble(context, "heading", this.getHeading().getDegrees()); } if (this.getPitch() != null) { rs.addStateValueAsDouble(context, "pitch", this.getPitch().getDegrees()); } } protected void doRestoreState(RestorableSupport rs, RestorableSupport.StateObject context) { // Restore the property limits and collision detection flags before restoring the view's position and // orientation. This has the effect of ensuring that the view's position and orientation are consistent with the // current property limits and the current surface collision state. RestorableSupport.StateObject so = rs.getStateObject(context, "viewPropertyLimits"); if (so != null) { this.getViewPropertyLimits().restoreState(rs, so); } Boolean b = rs.getStateValueAsBoolean(context, "detectCollisions"); if (b != null) { this.setDetectCollisions(b); } Double d = rs.getStateValueAsDouble(context, "fieldOfView"); if (d != null) { this.setFieldOfView(Angle.fromDegrees(d)); } d = rs.getStateValueAsDouble(context, "nearClipDistance"); if (d != null) { this.setNearClipDistance(d); } d = rs.getStateValueAsDouble(context, "farClipDistance"); if (d != null) { this.setFarClipDistance(d); } Position p = rs.getStateValueAsPosition(context, "eyePosition"); if (p != null) { this.setEyePosition(p); } d = rs.getStateValueAsDouble(context, "heading"); if (d != null) { this.setHeading(Angle.fromDegrees(d)); } d = rs.getStateValueAsDouble(context, "pitch"); if (d != null) { this.setPitch(Angle.fromDegrees(d)); } } @Override public Matrix pushReferenceCenter(DrawContext dc, Vec4 referenceCenter) { if (dc == null) { String message = Logging.getMessage("nullValue.DrawContextIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (dc.getGL() == null) { String message = Logging.getMessage("nullValue.DrawingContextGLIsNull"); Logging.logger().severe(message); throw new IllegalStateException(message); } if (referenceCenter == null) { String message = Logging.getMessage("nullValue.PointIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } Matrix modelview = getModelviewMatrix(); // Compute a new model-view matrix with origin at referenceCenter. Matrix matrix = null; if (modelview != null) { matrix = modelview.multiply(Matrix.fromTranslation(referenceCenter)); } GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility. // Store the current matrix-mode state. OGLStackHandler ogsh = new OGLStackHandler(); try { ogsh.pushAttrib(gl, GL2.GL_TRANSFORM_BIT); gl.glMatrixMode(GL2.GL_MODELVIEW); // Push and load a new model-view matrix to the current OpenGL context held by 'dc'. gl.glPushMatrix(); if (matrix != null) { double[] matrixArray = new double[16]; matrix.toArray(matrixArray, 0, false); gl.glLoadMatrixd(matrixArray, 0); } } finally { ogsh.pop(gl); } return matrix; } @Override public Matrix setReferenceCenter(DrawContext dc, Vec4 referenceCenter) { if (dc == null) { String message = Logging.getMessage("nullValue.DrawContextIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (dc.getGL() == null) { String message = Logging.getMessage("nullValue.DrawingContextGLIsNull"); Logging.logger().severe(message); throw new IllegalStateException(message); } if (referenceCenter == null) { String message = Logging.getMessage("nullValue.PointIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } Matrix modelview = getModelviewMatrix(); // Compute a new model-view matrix with origin at referenceCenter. Matrix matrix = null; if (modelview != null) { matrix = modelview.multiply(Matrix.fromTranslation(referenceCenter)); } if (matrix == null) { return null; } GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility. gl.glMatrixMode(GL2.GL_MODELVIEW); double[] matrixArray = new double[16]; matrix.toArray(matrixArray, 0, false); gl.glLoadMatrixd(matrixArray, 0); return matrix; } /** * Removes the model-view matrix on top of the matrix stack, and restores * the original matrix. * * @param dc * the current World Wind drawing context on which the original * matrix will be restored. * * @throws IllegalArgumentException * if <code>dc</code> is null, or if the <code>Globe</code> or * <code>GL</code> instances in <code>dc</code> are null. */ @Override public void popReferenceCenter(DrawContext dc) { if (dc == null) { String message = Logging.getMessage("nullValue.DrawContextIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (dc.getGL() == null) { String message = Logging.getMessage("nullValue.DrawingContextGLIsNull"); Logging.logger().severe(message); throw new IllegalStateException(message); } GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility. // Store the current matrix-mode state. OGLStackHandler ogsh = new OGLStackHandler(); try { ogsh.pushAttrib(gl, GL2.GL_TRANSFORM_BIT); gl.glMatrixMode(GL2.GL_MODELVIEW); // Pop the top model-view matrix. gl.glPopMatrix(); } finally { ogsh.pop(gl); } } /** * Transforms the specified object coordinates into window coordinates using * the given modelview and projection matrices, and viewport. * * @param point * The object coordinate to transform * @param modelview * The modelview matrix * @param projection * The projection matrix * @param viewport * The viewport * * @return the transformed coordinates */ public Vec4 project(Vec4 point, Matrix modelview, Matrix projection, java.awt.Rectangle viewport) { if (point == null) { String message = Logging.getMessage("nullValue.PointIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (modelview == null || projection == null) { String message = Logging.getMessage("nullValue.MatrixIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (viewport == null) { String message = Logging.getMessage("nullValue.RectangleIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } // GLU expects matrices as column-major arrays. double[] modelviewArray = new double[16]; double[] projectionArray = new double[16]; modelview.toArray(modelviewArray, 0, false); projection.toArray(projectionArray, 0, false); // GLU expects the viewport as a four-component array. int[] viewportArray = new int[] { viewport.x, viewport.y, viewport.width, viewport.height }; double[] result = new double[3]; if (!this.dc.getGLU().gluProject( point.x, point.y, point.z, modelviewArray, 0, projectionArray, 0, viewportArray, 0, result, 0)) { return null; } return Vec4.fromArray3(result, 0); } /** * Maps the given window coordinates into model coordinates using the given * matrices and viewport. * * @param windowPoint * the window point * @param modelview * the modelview matrix * @param projection * the projection matrix * @param viewport * the window viewport * * @return the unprojected point */ public Vec4 unProject(Vec4 windowPoint, Matrix modelview, Matrix projection, java.awt.Rectangle viewport) { if (windowPoint == null) { String message = Logging.getMessage("nullValue.PointIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (modelview == null || projection == null) { String message = Logging.getMessage("nullValue.MatrixIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (viewport == null) { String message = Logging.getMessage("nullValue.RectangleIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } // GLU expects matrices as column-major arrays. double[] modelviewArray = new double[16]; double[] projectionArray = new double[16]; modelview.toArray(modelviewArray, 0, false); projection.toArray(projectionArray, 0, false); // GLU expects the viewport as a four-component array. int[] viewportArray = new int[] { viewport.x, viewport.y, viewport.width, viewport.height }; double[] result = new double[3]; if (!this.dc.getGLU().gluUnProject( windowPoint.x, windowPoint.y, windowPoint.z, modelviewArray, 0, projectionArray, 0, viewportArray, 0, result, 0)) { return null; } return Vec4.fromArray3(result, 0); } /** * Sets the the opengl modelview and projection matrices to the given * matrices. * * @param dc * the drawing context * @param modelview * the modelview matrix * @param projection * the projection matrix */ public static void loadGLViewState(DrawContext dc, Matrix modelview, Matrix projection) { if (dc == null) { String message = Logging.getMessage("nullValue.DrawContextIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (dc.getGL() == null) { String message = Logging.getMessage("nullValue.DrawingContextGLIsNull"); Logging.logger().severe(message); throw new IllegalStateException(message); } if (modelview == null) { Logging.logger().fine("nullValue.ModelViewIsNull"); } if (projection == null) { Logging.logger().fine("nullValue.ProjectionIsNull"); } double[] matrixArray = new double[16]; GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility. // Store the current matrix-mode state. OGLStackHandler ogsh = new OGLStackHandler(); try { ogsh.pushAttrib(gl, GL2.GL_TRANSFORM_BIT); // Apply the model-view matrix to the current OpenGL context. gl.glMatrixMode(GL2.GL_MODELVIEW); if (modelview != null) { modelview.toArray(matrixArray, 0, false); gl.glLoadMatrixd(matrixArray, 0); } else { gl.glLoadIdentity(); } // Apply the projection matrix to the current OpenGL context. gl.glMatrixMode(GL2.GL_PROJECTION); if (projection != null) { projection.toArray(matrixArray, 0, false); gl.glLoadMatrixd(matrixArray, 0); } else { gl.glLoadIdentity(); } } finally { ogsh.pop(gl); } } /** * Add an animator to the this View. The View does not start the animator. * * @param animator * the {@link gov.nasa.worldwind.animation.Animator} to be added */ @Override public void addAnimator(Animator animator) { viewInputHandler.addAnimator(animator); } }