diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3dec7b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +gen-external-apklibs/ +bin/ +gen/ +target/ diff --git a/WorldWindAndroid/AndroidManifest.xml b/WorldWindAndroid/AndroidManifest.xml index c8da25c..c4eff7d 100644 --- a/WorldWindAndroid/AndroidManifest.xml +++ b/WorldWindAndroid/AndroidManifest.xml @@ -1,17 +1,8 @@ - - - - + + - + - - - + \ No newline at end of file diff --git a/WorldWindAndroid/library.iml b/WorldWindAndroid/library.iml new file mode 100644 index 0000000..507ce46 --- /dev/null +++ b/WorldWindAndroid/library.iml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WorldWindAndroid/pom.xml b/WorldWindAndroid/pom.xml new file mode 100644 index 0000000..3dda3c0 --- /dev/null +++ b/WorldWindAndroid/pom.xml @@ -0,0 +1,58 @@ + + + + + + 4.0.0 + + + com.github.trilogisit.worldwindandroid + parent + 1.0.0 + + + library + aar + + WorldWindAndroid + + + https://github.com/TrilogisIT/WorldWindAndroid.git + scm:git:git://github.com/TrilogisIT/WorldWindAndroid.git + scm:git:git://github.com/TrilogisIT/WorldWindAndroid.git + + + + https://www.github.com/TrilogisIT/WorldWindAndroid/issues + GitHub Issues + + + + + android + android + 4.4_r1 + provided + + + commons-logging + commons-logging + + + + + + + + + com.jayway.maven.plugins.android.generation2 + android-maven-plugin + + + + diff --git a/WorldWindAndroid/res/drawable-hdpi/ic_action_download.png b/WorldWindAndroid/res/drawable-hdpi/ic_action_download.png new file mode 100644 index 0000000..1f3d065 Binary files /dev/null and b/WorldWindAndroid/res/drawable-hdpi/ic_action_download.png differ diff --git a/WorldWindAndroid/res/drawable-hdpi/ic_action_network_wifi.png b/WorldWindAndroid/res/drawable-hdpi/ic_action_network_wifi.png new file mode 100644 index 0000000..c6e972a Binary files /dev/null and b/WorldWindAndroid/res/drawable-hdpi/ic_action_network_wifi.png differ diff --git a/WorldWindAndroid/res/drawable-hdpi/ic_action_network_wifi_off.png b/WorldWindAndroid/res/drawable-hdpi/ic_action_network_wifi_off.png new file mode 100644 index 0000000..989b26f Binary files /dev/null and b/WorldWindAndroid/res/drawable-hdpi/ic_action_network_wifi_off.png differ diff --git a/WorldWindAndroid/res/drawable-mdpi/ic_action_download.png b/WorldWindAndroid/res/drawable-mdpi/ic_action_download.png new file mode 100644 index 0000000..c2ead0c Binary files /dev/null and b/WorldWindAndroid/res/drawable-mdpi/ic_action_download.png differ diff --git a/WorldWindAndroid/res/drawable-mdpi/ic_action_network_wifi.png b/WorldWindAndroid/res/drawable-mdpi/ic_action_network_wifi.png new file mode 100644 index 0000000..7ca3158 Binary files /dev/null and b/WorldWindAndroid/res/drawable-mdpi/ic_action_network_wifi.png differ diff --git a/WorldWindAndroid/res/drawable-mdpi/ic_action_network_wifi_off.png b/WorldWindAndroid/res/drawable-mdpi/ic_action_network_wifi_off.png new file mode 100644 index 0000000..d5ba038 Binary files /dev/null and b/WorldWindAndroid/res/drawable-mdpi/ic_action_network_wifi_off.png differ diff --git a/WorldWindAndroid/res/drawable-xhdpi/ic_action_download.png b/WorldWindAndroid/res/drawable-xhdpi/ic_action_download.png new file mode 100644 index 0000000..38a3aee Binary files /dev/null and b/WorldWindAndroid/res/drawable-xhdpi/ic_action_download.png differ diff --git a/WorldWindAndroid/res/drawable-xhdpi/ic_action_network_wifi.png b/WorldWindAndroid/res/drawable-xhdpi/ic_action_network_wifi.png new file mode 100644 index 0000000..e335ca0 Binary files /dev/null and b/WorldWindAndroid/res/drawable-xhdpi/ic_action_network_wifi.png differ diff --git a/WorldWindAndroid/res/drawable-xhdpi/ic_action_network_wifi_off.png b/WorldWindAndroid/res/drawable-xhdpi/ic_action_network_wifi_off.png new file mode 100644 index 0000000..27bd53f Binary files /dev/null and b/WorldWindAndroid/res/drawable-xhdpi/ic_action_network_wifi_off.png differ diff --git a/WorldWindAndroid/res/drawable-xxhdpi/ic_action_download.png b/WorldWindAndroid/res/drawable-xxhdpi/ic_action_download.png new file mode 100644 index 0000000..ef7785a Binary files /dev/null and b/WorldWindAndroid/res/drawable-xxhdpi/ic_action_download.png differ diff --git a/WorldWindAndroid/res/drawable-xxhdpi/ic_action_network_wifi.png b/WorldWindAndroid/res/drawable-xxhdpi/ic_action_network_wifi.png new file mode 100644 index 0000000..ff9da3b Binary files /dev/null and b/WorldWindAndroid/res/drawable-xxhdpi/ic_action_network_wifi.png differ diff --git a/WorldWindAndroid/res/drawable-xxhdpi/ic_action_network_wifi_off.png b/WorldWindAndroid/res/drawable-xxhdpi/ic_action_network_wifi_off.png new file mode 100644 index 0000000..1f74046 Binary files /dev/null and b/WorldWindAndroid/res/drawable-xxhdpi/ic_action_network_wifi_off.png differ diff --git a/WorldWindAndroid/res/drawable/ic_action_download_status.xml b/WorldWindAndroid/res/drawable/ic_action_download_status.xml new file mode 100644 index 0000000..d5e5fc4 --- /dev/null +++ b/WorldWindAndroid/res/drawable/ic_action_download_status.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/WorldWindAndroid/res/drawable/ic_action_network_wifi_off_status.xml b/WorldWindAndroid/res/drawable/ic_action_network_wifi_off_status.xml new file mode 100644 index 0000000..f79401c --- /dev/null +++ b/WorldWindAndroid/res/drawable/ic_action_network_wifi_off_status.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/WorldWindAndroid/res/drawable/ic_action_network_wifi_status.xml b/WorldWindAndroid/res/drawable/ic_action_network_wifi_status.xml new file mode 100644 index 0000000..6d3cf3c --- /dev/null +++ b/WorldWindAndroid/res/drawable/ic_action_network_wifi_status.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/WorldWindAndroid/res/layout/statusbar.xml b/WorldWindAndroid/res/layout/statusbar.xml new file mode 100644 index 0000000..2bf8182 --- /dev/null +++ b/WorldWindAndroid/res/layout/statusbar.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WorldWindAndroid/res/raw/depth_frag.glsl b/WorldWindAndroid/res/raw/depth_frag.glsl new file mode 100644 index 0000000..a5cd4b3 --- /dev/null +++ b/WorldWindAndroid/res/raw/depth_frag.glsl @@ -0,0 +1,8 @@ + +precision mediump float; + + +void main() +{ + gl_FragColor = vec4(gl_FragCoord.z, 1); +} diff --git a/WorldWindAndroid/src/shaders/WorldMapLayerTexture.frag b/WorldWindAndroid/res/raw/diffuse_tex_frag.glsl similarity index 73% rename from WorldWindAndroid/src/shaders/WorldMapLayerTexture.frag rename to WorldWindAndroid/res/raw/diffuse_tex_frag.glsl index fc7f8fd..e352835 100644 --- a/WorldWindAndroid/src/shaders/WorldMapLayerTexture.frag +++ b/WorldWindAndroid/res/raw/diffuse_tex_frag.glsl @@ -4,11 +4,13 @@ varying vec2 vTextureCoord; uniform sampler2D sTexture; +uniform lowp float uOpacity; + /* * OpenGL ES fragment shader entry point. Called for each fragment rasterized when this shader's program is bound. */ void main() { gl_FragColor = texture2D(sTexture, vTextureCoord); - /*gl_FragColor = vec4(1,0,0,1);*/ + gl_FragColor = vec4(gl_FragColor.rgb, gl_FragColor.a * uOpacity); } diff --git a/WorldWindAndroid/src/shaders/WorldMapLayerTexture.vert b/WorldWindAndroid/res/raw/diffuse_tex_vert.glsl similarity index 85% rename from WorldWindAndroid/src/shaders/WorldMapLayerTexture.vert rename to WorldWindAndroid/res/raw/diffuse_tex_vert.glsl index fa47092..04b850a 100644 --- a/WorldWindAndroid/src/shaders/WorldMapLayerTexture.vert +++ b/WorldWindAndroid/res/raw/diffuse_tex_vert.glsl @@ -3,12 +3,14 @@ * TODO. */ attribute vec4 vertexPoint; -attribute vec2 aTextureCoord; +attribute vec4 aTextureCoord; + /* * Input uniform matrix defining the current modelview-projection transform matrix. Maps model coordinates to eye * coordinates. */ uniform mat4 mvpMatrix; +uniform mat4 texMatrix; varying vec2 vTextureCoord; /* @@ -18,5 +20,5 @@ void main() { /* Transform the surface vertex point from model coordinates to eye coordinates. */ gl_Position = mvpMatrix * vertexPoint; - vTextureCoord = aTextureCoord; + vTextureCoord = (texMatrix * aTextureCoord).st; } diff --git a/WorldWindAndroid/res/raw/etc1alphafrag.glsl b/WorldWindAndroid/res/raw/etc1alphafrag.glsl new file mode 100644 index 0000000..66ccbc7 --- /dev/null +++ b/WorldWindAndroid/res/raw/etc1alphafrag.glsl @@ -0,0 +1,16 @@ +precision mediump float; + +varying vec2 vTextureCoord; + +uniform sampler2D sTexture; +uniform sampler2D aTexture; + +uniform lowp float uOpacity; + +/* + * Read alpha component from 2nd etc1 texture + */ +void main() +{ + gl_FragColor = vec4(texture2D(sTexture, vTextureCoord).rgb, texture2D(aTexture, vTextureCoord).r * uOpacity); +} diff --git a/WorldWindAndroid/src/shaders/AbstractShape.vert b/WorldWindAndroid/res/raw/simple_vert.glsl similarity index 100% rename from WorldWindAndroid/src/shaders/AbstractShape.vert rename to WorldWindAndroid/res/raw/simple_vert.glsl diff --git a/WorldWindAndroid/src/shaders/SurfaceTileRenderer.frag b/WorldWindAndroid/res/raw/surfacetilerendererfrag.glsl similarity index 96% rename from WorldWindAndroid/src/shaders/SurfaceTileRenderer.frag rename to WorldWindAndroid/res/raw/surfacetilerendererfrag.glsl index ce69784..9ee5c2e 100644 --- a/WorldWindAndroid/src/shaders/SurfaceTileRenderer.frag +++ b/WorldWindAndroid/res/raw/surfacetilerendererfrag.glsl @@ -31,6 +31,8 @@ varying vec2 tileCoord; */ varying vec2 texCoord; +uniform lowp float uOpacity; + /* * Local function that returns the tile's RGBA color at the specified texture coordinate. The tile coordinate vector is * used to determine if the current fragment is inside or outside the tile's sector. This returns transparent black @@ -60,4 +62,5 @@ void main() /* because calling discard in a branch has been shown to increase the frame time by 3x on the Samsung Galaxy Tab */ /* 10.1. */ gl_FragColor = tileColor(tileTexture, tileCoord, texCoord); + gl_FragColor = vec4(gl_FragColor.rgb, gl_FragColor.a * uOpacity); } diff --git a/WorldWindAndroid/src/shaders/SurfaceTileRenderer.vert b/WorldWindAndroid/res/raw/surfacetilerenderervert.glsl similarity index 100% rename from WorldWindAndroid/src/shaders/SurfaceTileRenderer.vert rename to WorldWindAndroid/res/raw/surfacetilerenderervert.glsl diff --git a/WorldWindAndroid/src/shaders/TextRenderer.frag b/WorldWindAndroid/res/raw/textrendererfrag.glsl similarity index 71% rename from WorldWindAndroid/src/shaders/TextRenderer.frag rename to WorldWindAndroid/res/raw/textrendererfrag.glsl index df0bac4..d49812b 100644 --- a/WorldWindAndroid/src/shaders/TextRenderer.frag +++ b/WorldWindAndroid/res/raw/textrendererfrag.glsl @@ -2,7 +2,7 @@ precision mediump float; varying vec2 vTextureCoord; -varying vec4 vTextureColor; +uniform vec4 uTextureColor; uniform sampler2D sTexture; @@ -11,5 +11,5 @@ uniform sampler2D sTexture; */ void main() { - gl_FragColor = texture2D(sTexture, vTextureCoord) * vTextureColor; + gl_FragColor = texture2D(sTexture, vTextureCoord) * uTextureColor; } diff --git a/WorldWindAndroid/src/shaders/CompassLayerTexture.vert b/WorldWindAndroid/res/raw/textrenderervert.glsl similarity index 100% rename from WorldWindAndroid/src/shaders/CompassLayerTexture.vert rename to WorldWindAndroid/res/raw/textrenderervert.glsl diff --git a/WorldWindAndroid/src/shaders/AbstractShape.frag b/WorldWindAndroid/res/raw/uniform_color_frag.glsl similarity index 86% rename from WorldWindAndroid/src/shaders/AbstractShape.frag rename to WorldWindAndroid/res/raw/uniform_color_frag.glsl index a39cc76..0febbe1 100644 --- a/WorldWindAndroid/src/shaders/AbstractShape.frag +++ b/WorldWindAndroid/res/raw/uniform_color_frag.glsl @@ -16,12 +16,14 @@ precision mediump float; * Input uniform vec4 defining the current color. Every fragment rasterized by this fragment shader is displayed in this * color. */ -uniform vec4 color; +uniform vec4 uColor; + +uniform lowp float uOpacity; /* * OpenGL ES fragment shader entry point. Called for each fragment rasterized when this shader's program is bound. */ void main() { - gl_FragColor = color; + gl_FragColor = vec4(uColor.rgb, uColor.a * uOpacity); } diff --git a/WorldWindAndroid/src/shaders/WorldMapLayerColor.frag b/WorldWindAndroid/res/raw/vertex_color_frag.glsl similarity index 55% rename from WorldWindAndroid/src/shaders/WorldMapLayerColor.frag rename to WorldWindAndroid/res/raw/vertex_color_frag.glsl index b90c8c0..28a723b 100644 --- a/WorldWindAndroid/src/shaders/WorldMapLayerColor.frag +++ b/WorldWindAndroid/res/raw/vertex_color_frag.glsl @@ -6,11 +6,21 @@ precision mediump float; */ varying vec4 primColor; +uniform lowp float uOpacity; + +uniform vec4 uColor; + +uniform bool uUseVertexColor; + /* * OpenGL ES fragment shader entry point. Called for each fragment rasterized when this shader's program is bound. */ void main() { - /* Assign the fragment color to the varying vertex color. */ - gl_FragColor = primColor; + /* Assign the fragment color to the varying vertex color or uniform color. */ + if(uUseVertexColor) + gl_FragColor = primColor; + else + gl_FragColor = uColor; + gl_FragColor = vec4(gl_FragColor.rgb, gl_FragColor.a * uOpacity); } diff --git a/WorldWindAndroid/src/shaders/TiledTessellatorPick.vert b/WorldWindAndroid/res/raw/vertex_color_vert.glsl similarity index 96% rename from WorldWindAndroid/src/shaders/TiledTessellatorPick.vert rename to WorldWindAndroid/res/raw/vertex_color_vert.glsl index 8b56cf4..c1898e1 100644 --- a/WorldWindAndroid/src/shaders/TiledTessellatorPick.vert +++ b/WorldWindAndroid/res/raw/vertex_color_vert.glsl @@ -29,6 +29,8 @@ attribute vec4 vertexColor; */ uniform mat4 mvpMatrix; +uniform float uPointSize; + /* * Output variable vector to TiledTessellatorPick.frag defining the color for each primitive (triangle). This is * specified for each vertex and is interpolated for each rasterized fragment of each primitive. @@ -40,6 +42,8 @@ varying vec4 primColor; */ void main() { + gl_PointSize = uPointSize; + /* Transform the surface vertex point from model coordinates to eye coordinates. */ gl_Position = mvpMatrix * vertexPoint; diff --git a/WorldWindAndroid/res/values/dimens.xml b/WorldWindAndroid/res/values/dimens.xml new file mode 100644 index 0000000..1f39885 --- /dev/null +++ b/WorldWindAndroid/res/values/dimens.xml @@ -0,0 +1,10 @@ + + + + + 24dp + \ No newline at end of file diff --git a/WorldWindAndroid/res/values/strings.xml b/WorldWindAndroid/res/values/strings.xml new file mode 100644 index 0000000..b232aa9 --- /dev/null +++ b/WorldWindAndroid/res/values/strings.xml @@ -0,0 +1,11 @@ + + + + + +Altitude + \ No newline at end of file diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/BasicModel.java b/WorldWindAndroid/src/gov/nasa/worldwind/BasicModel.java index a894962..f2638fc 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/BasicModel.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/BasicModel.java @@ -18,6 +18,9 @@ public class BasicModel extends WWObjectImpl implements Model { protected Globe globe; protected LayerList layers; + protected boolean showTessellationBoundingVolumes; + protected boolean showTessellationTileIds; + protected boolean showWireframe; public BasicModel() { @@ -101,4 +104,55 @@ public void setLayers(LayerList layers) this.layers = layers; this.firePropertyChange(AVKey.LAYERS, old, this.layers); } + + /** + * Specifies whether to display as wireframe the exterior geometry of the tessellated globe surface. + * + * @param show true causes the geometry to be shown, false, the default, does not. + */ + public void setShowWireframe(boolean show) + { + this.showWireframe = show; + } + + /** + * Indicates whether the globe surface's interior geometry is to be drawn. + * + * @return true if it is to be drawn, otherwise false. + */ + public boolean isShowWireframe() + { + return this.showWireframe; + } + + /** + * Indicates whether the bounding volumes of the tessellated globe's surface geometry should be displayed. + * + * @return true if the bounding volumes are to be drawn, otherwise false. + */ + public boolean isShowTessellationBoundingVolumes() + { + return showTessellationBoundingVolumes; + } + + /** + * Specifies whether the bounding volumes of the globes tessellated surface geometry is to be drawn. + * + * @param showTessellationBoundingVolumes + * true if the bounding volumes should be drawn, false, the default, if not. + */ + public void setShowTessellationBoundingVolumes(boolean showTessellationBoundingVolumes) + { + this.showTessellationBoundingVolumes = showTessellationBoundingVolumes; + } + + @Override + public boolean isShowTessellationTileIds() { + return this.showTessellationTileIds; + } + + @Override + public void setShowTessellationTileIds(boolean showTileIds) { + this.showTessellationTileIds = showTileIds; + } } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/BasicView.java b/WorldWindAndroid/src/gov/nasa/worldwind/BasicView.java index 29a9c35..c69eb38 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/BasicView.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/BasicView.java @@ -4,19 +4,25 @@ */ package gov.nasa.worldwind; +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Point; +import gov.nasa.worldwind.animation.AngleEvaluator; +import gov.nasa.worldwind.animation.DoubleEvaluator; +import gov.nasa.worldwind.animation.PositionEvaluator; import gov.nasa.worldwind.avlist.AVKey; -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.Rect; -import gov.nasa.worldwind.geom.Vec4; +import gov.nasa.worldwind.geom.*; import gov.nasa.worldwind.globes.Globe; import gov.nasa.worldwind.render.DrawContext; import gov.nasa.worldwind.util.Logging; import gov.nasa.worldwind.util.WWMath; -import android.graphics.Point; +import gov.nasa.worldwind.view.BasicOrbitViewLimits; +import gov.nasa.worldwind.view.OrbitViewCollisionSupport; +import gov.nasa.worldwind.view.OrbitViewLimits; +import gov.nasa.worldwind.view.ViewPropertyLimits; + +import java.util.HashMap; +import java.util.Map; /** * Edited By: Nicola Dorigatti, Trilogis @@ -28,7 +34,7 @@ public class BasicView extends WWObjectImpl implements View { // TODO: add documentation to all public methods // TODO: make configurable - protected static final double MINIMUM_NEAR_DISTANCE = 2; + protected static final double MINIMUM_NEAR_DISTANCE = 1; protected static final double MINIMUM_FAR_DISTANCE = 100; // View representation @@ -51,17 +57,58 @@ public class BasicView extends WWObjectImpl implements View { protected double range; protected Angle heading = new Angle(); protected Angle tilt = new Angle(); + protected Vec4 lastForwardVector; + protected Vec4 lastUpVector; protected Angle roll = new Angle(); + protected DrawContext dc; + protected Globe globe; + protected OrbitViewCollisionSupport collisionSupport = new OrbitViewCollisionSupport(); + protected boolean detectCollisions = true; + protected boolean hadCollisions; + protected float farDistanceMultiplier = 1f; + protected ViewPropertyLimits viewLimits = new BasicOrbitViewLimits(); // Temporary property used to avoid constant allocation of Line objects during repeated calls to // computePositionFromScreenPoint. protected Line line = new Line(); + private Matrix tmpMatrix1 = Matrix.fromIdentity(); + private Matrix tmpMatrix2 = Matrix.fromIdentity(); public BasicView() { this.lookAtPosition.setDegrees(Configuration.getDoubleValue(AVKey.INITIAL_LATITUDE, 0.0), Configuration.getDoubleValue(AVKey.INITIAL_LONGITUDE, 0.0), 0.0); this.range = Configuration.getDoubleValue(AVKey.INITIAL_ALTITUDE, 0.0); } + public float getFarDistanceMultiplier() { + return this.farDistanceMultiplier; + } + + public void setFarDistanceMultiplier(float multiplier) { + this.farDistanceMultiplier = multiplier; + } + + 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; + } + + protected void flagHadCollisions() + { + this.hadCollisions = true; + } + /** {@inheritDoc} */ public Matrix getModelviewMatrix() { return this.modelview; @@ -193,6 +240,24 @@ public boolean computePositionFromScreenPoint(Globe globe, Point point, Position // noinspection SimplifiableIfStatement if (!this.computeRayFromScreenPoint(point, this.line)) return false; +// if (getEyePosition().elevation < globe.getMaxElevation() * 10) +// { + // Use ray casting below some altitude + // Try ray intersection with current terrain geometry +// Intersection[] intersections = getSceneController().getTerrain().intersect(this.line); +// if (intersections != null && intersections.length > 0) +// pickPos = globe.computePositionFromPoint(intersections[0].getIntersectionPoint()); +// else + // Fallback on raycasting using elevation data +// RayCastingSupport.intersectRayWithTerrain(globe, this.line.getOrigin(), this.line.getDirection(), +// 200, 20, result); +// return true; +// } + // Use intersection with sphere at reference altitude. +// Intersection inters[] = globe.intersect(this.line); +// if (inters != null) +// globe.computePositionFromPoint(inters[0].getIntersectionPoint(), result); + return globe.getIntersectionPosition(this.line, result); } @@ -225,6 +290,9 @@ public void apply(DrawContext dc) { throw new IllegalArgumentException(msg); } + this.dc = dc; + this.globe = this.dc.getGlobe(); + // Compute and apply the current modelview matrix, its inverse, and its transpose. this.applyModelviewMatrix(dc); this.modelviewInv.invertTransformMatrix(this.modelview); @@ -252,8 +320,7 @@ public void apply(DrawContext dc) { } protected void applyModelviewMatrix(DrawContext dc) { - this.modelview.setLookAt(dc.getVisibleTerrain(), this.lookAtPosition.latitude, this.lookAtPosition.longitude, this.lookAtPosition.elevation, AVKey.ABSOLUTE, this.range, - this.heading, this.tilt, this.roll); + calculateOrbitModelview(dc, this.lookAtPosition, this.heading, this.tilt, this.roll, this.range, this.modelview); } protected void applyProjectionMatrix(DrawContext dc) { @@ -265,13 +332,8 @@ protected void applyViewport(DrawContext dc) { } protected void applyFrustum(DrawContext dc) { - double tanHalfFov = this.fieldOfView.tanHalfAngle(); - this.nearClipDistance = this.eyePosition.elevation / (2 * Math.sqrt(2 * tanHalfFov * tanHalfFov + 1)); - this.farClipDistance = WWMath.computeHorizonDistance(dc.getGlobe(), this.eyePosition.elevation); - - if (this.nearClipDistance < MINIMUM_NEAR_DISTANCE) this.nearClipDistance = MINIMUM_NEAR_DISTANCE; - - if (this.farClipDistance < MINIMUM_FAR_DISTANCE) this.farClipDistance = MINIMUM_FAR_DISTANCE; + nearClipDistance = computeNearDistance(eyePoint); + farClipDistance = computeFarDistance(eyePosition); this.frustum.setPerspective(this.fieldOfView, this.viewport.width, this.viewport.height, this.nearClipDistance, this.farClipDistance); } @@ -282,25 +344,10 @@ public Vec4 getEyePoint() { } /** {@inheritDoc} */ - public Position getEyePosition(Globe globe) { - // TODO: Remove the globe parameter from this method. + public Position getEyePosition() { return this.eyePosition; } - public Position getLookAtPosition() { - return this.lookAtPosition; - } - - public void setLookAtPosition(Position position) { - if (position == null) { - String msg = Logging.getMessage("nullValue.PositionIsNull"); - Logging.error(msg); - throw new IllegalArgumentException(msg); - } - - this.lookAtPosition.set(position); - } - /** * Gets the geographic position of the current lookAt point on the globe. * @@ -397,6 +444,23 @@ public Angle getLookAtTilt(Globe globe) { return delta; } + public Position getLookAtPosition() { + return this.lookAtPosition; + } + + public void setLookAtPosition(Position position) { + if (position == null) { + String msg = Logging.getMessage("nullValue.PositionIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + this.lookAtPosition.set(position); + this.lookAtPosition.set(BasicOrbitViewLimits.limitLookAtPosition(this.lookAtPosition, this.getOrbitViewLimits())); + resolveCollisionsWithCenterPosition(); + firePropertyChange(AVKey.VIEW, null, this); + } + public double getRange() { return this.range; } @@ -407,8 +471,13 @@ public void setRange(double distance) { Logging.error(msg); throw new IllegalArgumentException(msg); } - + boolean isZoomingIn = distanceOrbitViewLimits that apply to this OrbitView. Incoming parameters to the + * methods setCenterPosition, setHeading, setPitch, or setZoom are be limited by the parameters defined in this + * OrbitViewLimits. + * + * @return the OrbitViewLimits that apply to this OrbitView + */ + public OrbitViewLimits getOrbitViewLimits() + { + return (OrbitViewLimits) viewLimits; + } + + /** + * Sets the OrbitViewLimits that will apply to this OrbitView. Incoming parameters to the + * methods setCenterPosition, setHeading, setPitch, or setZoom will be limited by the parameters defined in + * viewLimits. + * + * @param viewLimits the OrbitViewLimits that will apply to this OrbitView. + * + * @throws IllegalArgumentException if viewLimits is null. + */ + public void setOrbitViewLimits(OrbitViewLimits viewLimits) + { + if (viewLimits == null) + { + String message = Logging.getMessage("nullValue.ViewLimitsIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.viewLimits = viewLimits; + } + + public Vec4 getCurrentEyePoint() + { + if (this.globe != null) + { + Matrix modelview = Matrix.fromIdentity(); + calculateOrbitModelview(dc, this.lookAtPosition, this.heading, + this.tilt, this.roll, this.range, modelview); + if (modelview != null) { + Matrix modelviewInv = modelview.invert(); + if (modelviewInv != null) { + return Vec4.UNIT_W.transformBy4(modelviewInv); + } + } + } + + return Vec4.ZERO; + } + + public Position getCurrentEyePosition() + { + if (this.globe != null) + { + return this.globe.computePositionFromPoint(getCurrentEyePoint()); + } + + return Position.ZERO; + } + + public Vec4 getUpVector() + { + if (this.lastUpVector == null) + this.lastUpVector = Vec4.UNIT_Y.transformBy4(this.modelviewInv); + return this.lastUpVector; + } + + public Vec4 getForwardVector() + { + if (this.lastForwardVector == null) + this.lastForwardVector = Vec4.UNIT_NEGATIVE_Z.transformBy4(this.modelviewInv); + return this.lastForwardVector; + } + + /** + * Returns the most up-to-date forward vector. Unlike {@link #getForwardVector()}, this method will return the + * View's immediate forward vector. + * + * @return Vec4 of the forward axis. + */ + public Vec4 getCurrentForwardVector() + { + if (this.globe != null) + { + calculateOrbitModelview(this.dc, this.lookAtPosition, + this.heading, this.tilt, this.roll, this.range, tmpMatrix1); + if (tmpMatrix1 != null) + { + tmpMatrix1.invert(); + return Vec4.UNIT_NEGATIVE_Z.transformBy4(tmpMatrix1); + } + } + + return null; + } + + public static void calculateOrbitModelview(DrawContext dc, Position lookAtPosition, Angle heading, Angle tilt, Angle roll, double range, Matrix matrix) { + matrix.setLookAt(dc.getVisibleTerrain(), + lookAtPosition.latitude, lookAtPosition.longitude, lookAtPosition.elevation, + AVKey.CLAMP_TO_GROUND, range, heading, tilt, roll); + } + + protected double computeNearDistance(Vec4 eyePoint) + { + double near = 0; + if (eyePoint != null && this.dc != null) + { + double tanHalfFov = this.fieldOfView.tanHalfAngle(); + Vec4 lookAtPoint = dc.getVisibleTerrain().getSurfacePoint(lookAtPosition.latitude, lookAtPosition.longitude, 0); + double eyeDistance = new Vec4().subtract3AndSet(eyePoint, lookAtPoint).getLength3(); + near = eyeDistance / (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) + { + double height = OrbitViewCollisionSupport.computeViewHeightAboveSurface(dc, modelviewInv, fieldOfView, viewport, nearClipDistance); + far = WWMath.computeHorizonDistance(dc.getGlobe(), height); + far *= farDistanceMultiplier; + } + + return far < MINIMUM_FAR_DISTANCE ? MINIMUM_FAR_DISTANCE : far; + } + + protected void resolveCollisionsWithCenterPosition() + { + if (this.dc == null) + return; + + if (!isDetectCollisions()) + return; + + if(dc.getVisibleTerrain().getGlobe()==null) { + //TODO figure out why this is null + Logging.warning("getVisibleTerrain().getGlobe()==null.\tBasicView.dc.getGlobe(): " + dc.getGlobe()); + return; + } + // If there is no collision, 'newCenterPosition' will be null. Otherwise it will contain a value + // that will resolve the collision. + double nearDistance = this.computeNearDistance(this.getCurrentEyePoint()); + Position newCenter = this.collisionSupport.computeCenterPositionToResolveCollision(this, nearDistance, this.dc); + if (newCenter != null && newCenter.getLatitude().degrees >= -90 && newCenter.getLongitude().degrees <= 90) + { + this.lookAtPosition = newCenter; + flagHadCollisions(); + } + } + + protected void resolveCollisionsWithPitch() + { + if (this.dc == null) + return; + + if (!isDetectCollisions()) + return; + + if(dc.getVisibleTerrain().getGlobe()==null) { + //TODO figure out why this is null + Logging.warning("getVisibleTerrain().getGlobe()==null.\tBasicView.dc.getGlobe(): " + dc.getGlobe()); + return; + } + + // Compute the near distance corresponding to the current set of values. + // If there is no collision, 'newPitch' will be null. Otherwise it will contain a value + // that will resolve the collision. + double nearDistance = this.computeNearDistance(this.getCurrentEyePoint()); + Angle newPitch = this.collisionSupport.computePitchToResolveCollision(this, nearDistance, this.dc); + if (newPitch != null && newPitch.degrees <= 90 && newPitch.degrees >= 0) + { + this.tilt = newPitch; + flagHadCollisions(); + } + } + + protected Map goToAnimations = new HashMap(); + + public void stopAnimations() { + for(Animator a : goToAnimations.values()) { + a.cancel(); + } + goToAnimations.clear(); + } + + public ValueAnimator createTiltAnimator(final WorldWindowGLTextureView wwd, Angle tilt) { + final ValueAnimator tiltAnimator = ValueAnimator + .ofObject(new AngleEvaluator(), this.tilt, tilt); + tiltAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(final ValueAnimator animation) { + wwd.invokeInRenderingThread(new Runnable() { + @Override + public void run() { + setTilt((Angle)animation.getAnimatedValue()); + } + }); + BasicView.this.firePropertyChange(AVKey.VIEW, null, BasicView.this); + } + }); + return tiltAnimator; + } + + public ValueAnimator createHeadingAnimator(final WorldWindowGLTextureView wwd, Angle heading) { + final ValueAnimator headingAnimator = ValueAnimator + .ofObject(new AngleEvaluator(), this.heading, heading); + headingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(final ValueAnimator animation) { + wwd.invokeInRenderingThread(new Runnable() { + @Override + public void run() { + setHeading((Angle)animation.getAnimatedValue()); + } + }); + firePropertyChange(AVKey.VIEW, null, BasicView.this); + } + }); + return headingAnimator; + } + + public ValueAnimator createRangeAnimator(final WorldWindowGLTextureView wwd, double range) { + final ValueAnimator rangeAnimator = ValueAnimator + .ofObject(new DoubleEvaluator(), this.range, range); + rangeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(final ValueAnimator animation) { + wwd.invokeInRenderingThread(new Runnable() { + @Override + public void run() { + setRange((Double)animation.getAnimatedValue()); + } + }); + firePropertyChange(AVKey.VIEW, null, BasicView.this); + } + }); + return rangeAnimator; + } + + private ValueAnimator createLookAtAnimator(final WorldWindowGLTextureView wwd, Position position) { + final ValueAnimator lookAtAnimator = ValueAnimator.ofObject( + new PositionEvaluator(), this.lookAtPosition, position); + lookAtAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(final ValueAnimator animation) { + wwd.invokeInRenderingThread(new Runnable() { + @Override + public void run() { + setLookAtPosition((Position)animation.getAnimatedValue()); + } + }); + firePropertyChange(AVKey.VIEW, null, BasicView.this); + } + }); + return lookAtAnimator; + } + + public void animate(Animator animator) { + stopAnimations(); + goToAnimations.put("Custom", animator); + animator.start(); } } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/Model.java b/WorldWindAndroid/src/gov/nasa/worldwind/Model.java index 99cf429..d9bf9a3 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/Model.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/Model.java @@ -21,4 +21,16 @@ public interface Model extends WWObject LayerList getLayers(); void setLayers(LayerList layers); + + void setShowWireframe(boolean show); + + boolean isShowWireframe(); + + boolean isShowTessellationBoundingVolumes(); + + void setShowTessellationBoundingVolumes(boolean showTileBoundingVolumes); + + boolean isShowTessellationTileIds(); + + void setShowTessellationTileIds(boolean showTileIds); } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/SceneController.java b/WorldWindAndroid/src/gov/nasa/worldwind/SceneController.java index 4f6887a..4b2c585 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/SceneController.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/SceneController.java @@ -5,22 +5,35 @@ */ package gov.nasa.worldwind; +import android.graphics.Point; +import android.opengl.GLES20; +import android.os.SystemClock; import gov.nasa.worldwind.avlist.AVKey; import gov.nasa.worldwind.cache.GpuResourceCache; import gov.nasa.worldwind.layers.Layer; +import gov.nasa.worldwind.pick.DepthBufferSupport; +import gov.nasa.worldwind.pick.PickSupport; import gov.nasa.worldwind.pick.PickedObject; import gov.nasa.worldwind.pick.PickedObjectList; -import gov.nasa.worldwind.render.Color; -import gov.nasa.worldwind.render.DrawContext; -import gov.nasa.worldwind.render.OrderedRenderable; +import gov.nasa.worldwind.render.*; +import gov.nasa.worldwind.terrain.ElevationModel; +import gov.nasa.worldwind.terrain.SectorGeometry; import gov.nasa.worldwind.terrain.SectorGeometryList; +import gov.nasa.worldwind.util.GLRuntimeCapabilities; import gov.nasa.worldwind.util.Logging; -import android.graphics.Point; -import android.opengl.GLES20; +import gov.nasa.worldwind.util.OGLStackHandler; +import gov.nasa.worldwind.util.PerformanceStatistic; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; + +import static android.opengl.GLES20.*; +import static gov.nasa.worldwind.util.OGLStackHandler.GL_POLYGON_BIT; /** * Edited By: Nicola Dorigatti, Trilogis - * + * * @author dcollins * @version $Id: SceneController.java 834 2012-10-08 22:25:55Z dcollins $ */ @@ -31,22 +44,44 @@ public class SceneController extends WWObjectImpl { protected DrawContext dc; protected Color clearColor = new Color(); protected GpuResourceCache gpuResourceCache; + protected DepthBufferSupport mDepthSupport = new DepthBufferSupport(); protected boolean deepPick; + protected PickSupport pickSupport = new PickSupport(); protected Point pickPoint; protected PickedObjectList objectsAtPickPoint = new PickedObjectList(); + protected Set perFrameStatisticsKeys = new HashSet(); + protected final Map perFrameStatistics = Collections.synchronizedMap(new ConcurrentHashMap()); + + /** Support class used to build the composite representation of surface objects as a list of SurfaceTiles. */ + protected SurfaceObjectTileBuilder surfaceObjectTileBuilder; + /** The composite surface object representation. Populated each frame by the {@link #surfaceObjectTileBuilder}. */ + protected List surfaceObjectTiles = new ArrayList(); + /** The display name for the surface object tile count performance statistic. */ + protected static final String SURFACE_OBJECT_TILE_COUNT_NAME = "Surface Object Tiles"; + protected SceneController() { this.setVerticalExaggeration(Configuration.getDoubleValue(AVKey.VERTICAL_EXAGGERATION)); this.dc = this.createDrawContext(); } + protected PickSupport getPickSupport() { + pickSupport.setup((int)view.getViewport().width, (int)view.getViewport().height); + return pickSupport; + } + + protected DepthBufferSupport getDepthBufferSupport() { + mDepthSupport.setup((int)view.getViewport().width, (int)view.getViewport().height); + return mDepthSupport; + } + protected DrawContext createDrawContext() { return new DrawContext(); } /** * Indicates the scene controller's model. This returns null if the scene controller has no model. - * + * * @return the scene controller's model, or null if the scene controller has no model. */ public Model getModel() { @@ -55,7 +90,7 @@ public Model getModel() { /** * Specifies the scene controller's model. This method fires an {@link gov.nasa.worldwind.avlist.AVKey#MODEL} property change event. - * + * * @param model * the scene controller's model. */ @@ -71,7 +106,7 @@ public void setModel(Model model) { /** * Returns the current view. This method fires an {@link gov.nasa.worldwind.avlist.AVKey#VIEW} property change * event. - * + * * @return the current view. */ public View getView() { @@ -80,7 +115,7 @@ public View getView() { /** * Sets the current view. - * + * * @param view * the view. */ @@ -96,7 +131,7 @@ public void setView(View view) { /** * Indicates the current vertical exaggeration. - * + * * @return the current vertical exaggeration. */ public double getVerticalExaggeration() { @@ -105,19 +140,21 @@ public double getVerticalExaggeration() { /** * Specifies the exaggeration to apply to elevation values of terrain and other displayed items. - * + * * @param verticalExaggeration * the vertical exaggeration to apply. */ public void setVerticalExaggeration(double verticalExaggeration) { - Double oldVE = this.verticalExaggeration; - this.verticalExaggeration = verticalExaggeration; - this.firePropertyChange(AVKey.VERTICAL_EXAGGERATION, oldVE, verticalExaggeration); + if(this.verticalExaggeration != verticalExaggeration) { + double oldVE = this.verticalExaggeration; + this.verticalExaggeration = verticalExaggeration; + this.firePropertyChange(AVKey.VERTICAL_EXAGGERATION, oldVE, this.verticalExaggeration); + } } /** * Returns this scene controller's GPU Resource cache. - * + * * @return this scene controller's GPU Resource cache. */ public GpuResourceCache getGpuResourceCache() { @@ -126,7 +163,7 @@ public GpuResourceCache getGpuResourceCache() { /** * Specifies the GPU Resource cache to use. - * + * * @param gpuResourceCache * the texture cache. */ @@ -136,7 +173,7 @@ public void setGpuResourceCache(GpuResourceCache gpuResourceCache) { /** * Indicates whether all items under the cursor are identified during picking. - * + * * @return true if all items under the cursor are identified during picking, otherwise false. */ public boolean isDeepPickEnabled() { @@ -145,7 +182,7 @@ public boolean isDeepPickEnabled() { /** * Specifies whether all items under the cursor are identified during picking. - * + * * @param tf * true to identify all items under the cursor during picking, otherwise false. */ @@ -155,7 +192,7 @@ public void setDeepPickEnabled(boolean tf) { /** * Returns the current pick point in AWT screen coordinates. - * + * * @return the current pick point, or null if no pick point is current. * @see #setPickPoint(Point) */ @@ -169,7 +206,7 @@ public Point getPickPoint() { * them in a PickedObjectList. This list can be accessed by calling {@link #getObjectsAtPickPoint()}. *

* If the pick point is null, this scene controller ignores the pick point and the list of objects returned by getPickedObjectList is empty. - * + * * @param pickPoint * the current pick point, or null. */ @@ -180,16 +217,34 @@ public void setPickPoint(Point pickPoint) { /** * Returns the list of picked objects at the current pick point. The returned list is computed during the most * recent call to repaint. - * + * * @return the list of picked objects at the pick point, or null if no objects are currently picked. */ public PickedObjectList getObjectsAtPickPoint() { return this.objectsAtPickPoint; } + public void setPerFrameStatisticsKeys(Set keys) + { + this.perFrameStatisticsKeys.clear(); + if (keys == null) + return; + + for (String key : keys) + { + if (key != null) + this.perFrameStatisticsKeys.add(key); + } + } + + public Map getPerFrameStatistics() + { + return perFrameStatistics; + } + /** * Cause the window to regenerate the frame, including pick resolution. - * + * * @param viewportWidth * the width of the current viewport this scene controller is associated with, in pixels. Must * not be less than zero. @@ -197,9 +252,9 @@ public PickedObjectList getObjectsAtPickPoint() { * the height of the current viewport this scene controller is associated with, in pixels. * Must not be less than zero. * @throws IllegalArgumentException - * if either viewportWidth or viewportHeight are less than zero. + * if either viewportWidth or viewportHeight are last ess than zero. */ - public void drawFrame(int viewportWidth, int viewportHeight) { + public void drawFrame(double deltaTime, int viewportWidth, int viewportHeight) { if (viewportWidth < 0) { String msg = Logging.getMessage("generic.WidthIsInvalid", viewportWidth); Logging.error(msg); @@ -212,20 +267,52 @@ public void drawFrame(int viewportWidth, int viewportHeight) { throw new IllegalArgumentException(msg); } +// perFrameStatistics.clear(); + this.surfaceObjectTiles.clear(); // Clear the surface object tiles generated during the last frame. // Prepare the drawing context for a new frame then cause this scene controller to draw its content. There is no // need to explicitly swap the front and back buffers here, as the owner WorldWindow does this for us. In the // case of WorldWindowGLSurfaceView, the GLSurfaceView automatically swaps the front and back buffers for us. this.initializeDrawContext(this.dc, viewportWidth, viewportHeight); + pickSupport.setup(viewportWidth, viewportHeight); + mDepthSupport.setup(viewportWidth, viewportHeight); + dc.setDeltaTime(deltaTime); + this.doDrawFrame(this.dc); + + Set perfKeys = dc.getPerFrameStatisticsKeys(); + + if (perfKeys.contains(PerformanceStatistic.MEMORY_CACHE) || perfKeys.contains(PerformanceStatistic.ALL)) + { + this.dc.setPerFrameStatistics(WorldWind.getMemoryCacheSet().getPerformanceStatistics()); + } + + if (perfKeys.contains(PerformanceStatistic.TEXTURE_CACHE) || perfKeys.contains(PerformanceStatistic.ALL)) + { + if (dc.getTextureCache() != null) + this.dc.setPerFrameStatistic(PerformanceStatistic.TEXTURE_CACHE, + "Texture Cache size (Kb)", this.dc.getTextureCache().getUsedCapacity() / 1000); + } + + if (perfKeys.contains(PerformanceStatistic.JVM_HEAP) || perfKeys.contains(PerformanceStatistic.ALL)) + { + long totalMemory = Runtime.getRuntime().totalMemory(); + this.dc.setPerFrameStatistic(PerformanceStatistic.JVM_HEAP, + "JVM total memory (Kb)", totalMemory / 1000); + + this.dc.setPerFrameStatistic(PerformanceStatistic.JVM_HEAP_USED, + "JVM used memory (Kb)", (totalMemory - Runtime.getRuntime().freeMemory()) / 1000); + } } protected void doDrawFrame(DrawContext dc) { this.initializeFrame(dc); try { this.applyView(dc); + this.createPickFrustum(dc); this.createTerrain(dc); - this.clearFrame(dc); - this.pick(dc); + this.preRender(dc); +// this.clearFrame(dc); +// this.pick(dc); this.clearFrame(dc); this.draw(dc); } finally { @@ -234,34 +321,44 @@ protected void doDrawFrame(DrawContext dc) { } protected void initializeDrawContext(DrawContext dc, int viewportWidth, int viewportHeight) { - long timeStamp = System.currentTimeMillis(); - dc.initialize(viewportWidth, viewportHeight); + dc.setFrameTimeStamp(SystemClock.elapsedRealtime()); dc.setModel(this.model); dc.setView(this.view); dc.setVerticalExaggeration(this.verticalExaggeration); dc.setGpuResourceCache(this.gpuResourceCache); - dc.setFrameTimeStamp(timeStamp); dc.setPickPoint(this.pickPoint); + dc.setPerFrameStatisticsKeys(this.perFrameStatisticsKeys, this.perFrameStatistics); } protected void initializeFrame(DrawContext dc) { GLES20.glEnable(GLES20.GL_BLEND); + WorldWindowImpl.glCheckError("glEnable: GL_BLEND"); GLES20.glEnable(GLES20.GL_CULL_FACE); + WorldWindowImpl.glCheckError("glEnable: GL_CULL_FACE"); GLES20.glEnable(GLES20.GL_DEPTH_TEST); - GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA); // Blend in premultiplied alpha mode. + WorldWindowImpl.glCheckError("glEnable: GL_DEPTH_TEST"); + GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA); // Blend in premultiplied alpha mode. + WorldWindowImpl.glCheckError("glBlendFunc"); GLES20.glDepthFunc(GLES20.GL_LEQUAL); + WorldWindowImpl.glCheckError("glDepthFunc"); // We do not specify glCullFace, because the default cull face state GL_BACK is appropriate for our needs. } protected void finalizeFrame(DrawContext dc) { // Restore the default GL state values we modified in initializeFrame. GLES20.glDisable(GLES20.GL_BLEND); + WorldWindowImpl.glCheckError("glDisable: GL_BLEND"); GLES20.glDisable(GLES20.GL_CULL_FACE); + WorldWindowImpl.glCheckError("glDisable: GL_CULL_FACE"); GLES20.glDisable(GLES20.GL_DEPTH_TEST); + WorldWindowImpl.glCheckError("glDisable: GL_DEPTH_TEST"); GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ZERO); + WorldWindowImpl.glCheckError("glBlendFunc"); GLES20.glDepthFunc(GLES20.GL_LESS); + WorldWindowImpl.glCheckError("glDepthFunc"); GLES20.glClearColor(0f, 0f, 0f, 0f); + WorldWindowImpl.glCheckError("glClearColor"); } protected void clearFrame(DrawContext dc) { @@ -270,7 +367,9 @@ protected void clearFrame(DrawContext dc) { // Set the DrawContext's clear color, then clear the framebuffer's color buffer and depth buffer. This fills // the color buffer with the background color, and fills the depth buffer with 1 (the default). GLES20.glClearColor((float) this.clearColor.r, (float) this.clearColor.g, (float) this.clearColor.b, (float) this.clearColor.a); + WorldWindowImpl.glCheckError("glClearColor"); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); + WorldWindowImpl.glCheckError("glClear"); } protected void applyView(DrawContext dc) { @@ -294,9 +393,53 @@ protected void createTerrain(DrawContext dc) { dc.setVisibleSector(surfaceGeometry != null ? surfaceGeometry.getSector() : null); } + protected void preRender(DrawContext dc) + { + try + { + dc.setPreRenderMode(true); + + // Pre-render the layers. + if (dc.getLayers() != null) + { + for (Layer layer : dc.getLayers()) + { + try + { + dc.setCurrentLayer(layer); + layer.preRender(dc); + } + catch (Exception e) + { + String message = Logging.getMessage("SceneController.ExceptionWhilePreRenderingLayer", + (layer != null ? layer.getClass().getName() : Logging.getMessage("term.unknown"))); + Logging.error(message, e); + // Don't abort; continue on to the next layer. + } + } + dc.setCurrentLayer(null); + } + + // Pre-render the deferred/ordered surface renderables. + this.preRenderOrderedSurfaceRenderables(dc); + } + catch (Exception e) + { + Logging.error(Logging.getMessage("BasicSceneController.ExceptionDuringPreRendering"), + e); + } + finally + { + dc.setPreRenderMode(false); + } + } + protected void draw(DrawContext dc) { this.drawLayers(dc); + // Draw the deferred/ordered surface renderables. + this.drawOrderedSurfaceRenderables(dc); this.drawOrderedRenderables(dc); + this.drawDiagnosticDisplays(dc); } protected void drawLayers(DrawContext dc) { @@ -325,7 +468,9 @@ protected void drawOrderedRenderables(DrawContext dc) { OrderedRenderable or = dc.pollOrderedRenderables(); try { + dc.setCurrentLayer(or.getLayer()); or.render(dc); + dc.setCurrentLayer(null); } catch (Exception e) { String msg = Logging.getMessage("generic.ExceptionRenderingOrderedRenderable", or); Logging.error(msg, e); @@ -336,46 +481,44 @@ protected void drawOrderedRenderables(DrawContext dc) { dc.setOrderedRenderingMode(false); } + protected void drawDiagnosticDisplays(DrawContext dc) { + if (dc.getSurfaceGeometry() != null && dc.getModel() != null + && (dc.getModel().isShowWireframe() || dc.getModel().isShowTessellationBoundingVolumes() + || dc.getModel().isShowTessellationTileIds())) + { + Model model = dc.getModel(); + + for (SectorGeometry sg : dc.getSurfaceGeometry()) + { + if (model.isShowWireframe()) + sg.renderWireframe(dc); + + if (model.isShowTessellationBoundingVolumes()) + sg.renderBoundingVolume(dc); + + if(model.isShowTessellationTileIds()) + sg.renderTileID(dc); + } + } + } + protected void pick(DrawContext dc) { try { - this.beginPicking(dc); + dc.setPickingMode(true); + getPickSupport().beginPicking(dc); + getPickSupport().bindFrameBuffer(); this.doPick(dc); } finally { - this.endPicking(dc); + getPickSupport().unbindFrameBuffer(); + dc.setPickingMode(false); + getPickSupport().endPicking(dc); } } - /** - * Configures the draw context and GL state for picking. This ensures that pick colors are drawn into the - * framebuffer as specified, and are not modified by any GL state. This makes the following GL state changes: - *

    - *
  • Disable blending
  • - *
  • Disable dithering
  • - *
- * - * @param dc - * the draw context to configure. - */ - protected void beginPicking(DrawContext dc) { - dc.setPickingMode(true); - GLES20.glDisable(GLES20.GL_BLEND); // Blending is disabled by default, but is enabled in initializeFrame. - GLES20.glDisable(GLES20.GL_DITHER); // Dithering is enabled by default. - } - - /** - * Restores the draw context and GL state modified in beginPicking. This makes the following GL state changes: - *
    - *
  • Enable blending
  • - *
  • Enable dithering
  • - *
- * - * @param dc - * the draw context on which to restore state. - */ - protected void endPicking(DrawContext dc) { - dc.setPickingMode(false); - GLES20.glEnable(GLES20.GL_BLEND); // Blending is disabled by default, but is enabled in initializeFrame. - GLES20.glEnable(GLES20.GL_DITHER); // Dithering is enabled by default. + protected void createPickFrustum(DrawContext dc) + { + dc.addPickPointFrustum(); +// dc.addPickRectangleFrustum(); } protected void doPick(DrawContext dc) { @@ -395,9 +538,11 @@ protected void doPickTerrain(DrawContext dc) { protected void doPickNonTerrain(DrawContext dc) { if (dc.getPickPoint() == null) // Don't do the pick if there's no current pick point. - return; + return; this.pickLayers(dc); + // Pick against the deferred/ordered surface renderables. + this.pickOrderedSurfaceRenderables(dc); this.pickOrderedRenderables(dc); } @@ -475,13 +620,14 @@ protected void doDeepPick(DrawContext dc) { *
    *
  • Disable depth test
  • *
- * + * * @param dc * the draw context to configure. */ protected void beginDeepPicking(DrawContext dc) { dc.setDeepPickingEnabled(true); - GLES20.glDisable(GLES20.GL_DEPTH_TEST); // Depth test is disabled by default, but enabled in initializeFrame. + GLES20.glDisable(GLES20.GL_DEPTH_TEST); // Depth test is disabled by default, but enabled in initializeFrame. + WorldWindowImpl.glCheckError("glDisable: GL_DEPTH_TEST"); } /** @@ -490,13 +636,14 @@ protected void beginDeepPicking(DrawContext dc) { *
    *
  • Enable depth test
  • *
- * + * * @param dc * the draw context on which to restore state. */ protected void endDeepPicking(DrawContext dc) { dc.setDeepPickingEnabled(false); - GLES20.glEnable(GLES20.GL_DEPTH_TEST); // Depth test is disabled by default, but enabled in initializeFrame. + GLES20.glEnable(GLES20.GL_DEPTH_TEST); // Depth test is disabled by default, but enabled in initializeFrame. + WorldWindowImpl.glCheckError("glEnable: GL_DEPTH_TEST"); } protected PickedObjectList mergePickedObjectLists(PickedObjectList listA, PickedObjectList listB) { @@ -520,4 +667,179 @@ protected PickedObjectList mergePickedObjectLists(PickedObjectList listA, Picked return listA; } + + //**************************************************************// + //******************** Ordered Surface Renderable ************// + //**************************************************************// + + protected void preRenderOrderedSurfaceRenderables(DrawContext dc) + { + if (dc.getOrderedSurfaceRenderables().isEmpty()) + return; + + dc.setOrderedRenderingMode(true); + + // Build a composite representation of the SurfaceObjects. This operation potentially modifies the framebuffer + // contents to update surface tile textures, therefore it must be executed during the preRender phase. + this.buildCompositeSurfaceObjects(dc); + + // PreRender the individual deferred/ordered surface renderables. + int logCount = 0; + while (dc.getOrderedSurfaceRenderables().peek() != null) + { + try + { + OrderedRenderable or = dc.getOrderedSurfaceRenderables().poll(); + if (or instanceof PreRenderable) + ((PreRenderable) or).preRender(dc); + } + catch (Exception e) + { + Logging.warning(Logging.getMessage("BasicSceneController.ExceptionDuringPreRendering"), e); + + // Limit how many times we log a problem. + if (++logCount > Logging.getMaxMessageRepeatCount()) + break; + } + } + + dc.setOrderedRenderingMode(false); + } + + protected void pickOrderedSurfaceRenderables(DrawContext dc) + { + dc.setOrderedRenderingMode(true); + + // Pick the individual deferred/ordered surface renderables. We don't use the composite representation of + // SurfaceObjects because we need to distinguish between individual objects. Therefore we let each object handle + // drawing and resolving picking. + while (dc.getOrderedSurfaceRenderables().peek() != null) + { + OrderedRenderable or = dc.getOrderedSurfaceRenderables().poll(); + dc.setCurrentLayer(or.getLayer()); + or.pick(dc, dc.getPickPoint()); + dc.setCurrentLayer(null); + } + + dc.setOrderedRenderingMode(false); + } + + protected void drawOrderedSurfaceRenderables(DrawContext dc) + { + dc.setOrderedRenderingMode(true); + + // Draw the composite representation of the SurfaceObjects created during preRendering. + this.drawCompositeSurfaceObjects(dc); + + // Draw the individual deferred/ordered surface renderables. SurfaceObjects that add themselves to the ordered + // surface renderable queue during preRender are drawn in drawCompositeSurfaceObjects. Since this invokes + // SurfaceObject.render during preRendering, SurfaceObjects should not add themselves to the ordered surface + // renderable queue for rendering. We assume this queue is not populated with SurfaceObjects that participated + // in the composite representation created during preRender. + while (dc.getOrderedSurfaceRenderables().peek() != null) + { + try + { + OrderedRenderable or = dc.getOrderedSurfaceRenderables().poll(); + dc.setCurrentLayer(or.getLayer()); + or.render(dc); + } + catch (Exception e) + { + Logging.warning(Logging.getMessage("BasicSceneController.ExceptionDuringRendering"), e); + } finally { + dc.setCurrentLayer(null); + } + } + + dc.setOrderedRenderingMode(false); + } + + /** + * Builds a composite representation for all {@link gov.nasa.worldwind.render.SurfaceObject} instances in the draw + * context's ordered surface renderable queue. While building the composite representation this invokes {@link + * gov.nasa.worldwind.render.SurfaceObject#render(gov.nasa.worldwind.render.DrawContext)} in ordered rendering mode. + * This does nothing if the ordered surface renderable queue is empty, or if it does not contain any + * SurfaceObjects. + *

+ * This method is called during the preRender phase, and is therefore free to modify the framebuffer contents to + * create the composite representation. + * + * @param dc The drawing context containing the surface objects to build a composite representation for. + * + * @see gov.nasa.worldwind.render.DrawContext#getOrderedSurfaceRenderables() + */ + protected void buildCompositeSurfaceObjects(DrawContext dc) + { + // If the the draw context's ordered surface renderable queue is empty, then there are no surface objects to + // build a composite representation of. + if (dc.getOrderedSurfaceRenderables().isEmpty()) + return; + + // Lazily create the support object used to build the composite representation. We keep a reference to the + // SurfaceObjectTileBuilder used to build the tiles because it acts as a cache key to the tiles and determines + // when the tiles must be updated. The tile builder does not retain any references the SurfaceObjects, so + // keeping a reference to it does not leak memory should we never use it again. + if (this.surfaceObjectTileBuilder == null) + this.surfaceObjectTileBuilder = this.createSurfaceObjectTileBuilder(); + + // Build the composite representation as a list of surface tiles. + List tiles = this.surfaceObjectTileBuilder.buildTiles(dc, dc.getOrderedSurfaceRenderables()); + if (tiles != null) + this.surfaceObjectTiles.addAll(tiles); + + if(WorldWindowImpl.DEBUG) + Logging.verbose("Built composite surface object tiles #"+tiles.size()); + } + + /** + * Causes the scene controller to draw the composite representation of all {@link + * gov.nasa.worldwind.render.SurfaceObject} instances in the draw context's ordered surface renderable queue. This + * representation was built during the preRender phase. This does nothing if the ordered surface renderable queue is + * empty, or if it does not contain any SurfaceObjects. + * + * @param dc The drawing context containing the surface objects who's composite representation is drawn. + */ + protected void drawCompositeSurfaceObjects(DrawContext dc) + { + // The composite representation is stored as a list of surface tiles. If the list is empty, then there are no + // SurfaceObjects to draw. + if (this.surfaceObjectTiles.isEmpty()) + return; + + int attributeMask = + GL_COLOR_BUFFER_BIT // For alpha test enable, blend enable, alpha func, blend func, blend ref. + | GL_POLYGON_BIT; // For cull face enable, cull face, polygon mode. + + OGLStackHandler ogsh = new OGLStackHandler(); + ogsh.pushAttrib(attributeMask); + try + { + glEnable(GL_BLEND); + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + // Enable blending in premultiplied color mode. The color components in each surface object tile are + // premultiplied by the alpha component. + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + + dc.getSurfaceTileRenderer().renderTiles(dc, this.surfaceObjectTiles); + dc.setPerFrameStatistic(PerformanceStatistic.IMAGE_TILE_COUNT, SURFACE_OBJECT_TILE_COUNT_NAME, + this.surfaceObjectTiles.size()); + } + finally + { + ogsh.popAttrib(attributeMask); + } + } + + /** + * Returns a new {@link gov.nasa.worldwind.render.SurfaceObjectTileBuilder} configured to build a composite + * representation of {@link gov.nasa.worldwind.render.SurfaceObject} instances. + * + * @return A new {@link gov.nasa.worldwind.render.SurfaceObjectTileBuilder}. + */ + protected SurfaceObjectTileBuilder createSurfaceObjectTileBuilder() + { + return new SurfaceObjectTileBuilder(); + } } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/View.java b/WorldWindAndroid/src/gov/nasa/worldwind/View.java index 45726a8..db70a10 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/View.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/View.java @@ -219,5 +219,5 @@ public interface View extends WWObject { * * @return the position of the eye corresponding to the most recent state of this view */ - Position getEyePosition(Globe globe); + Position getEyePosition(); } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/WorldWind.java b/WorldWindAndroid/src/gov/nasa/worldwind/WorldWind.java index b1670d9..96e9d6a 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/WorldWind.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/WorldWind.java @@ -98,7 +98,7 @@ public static FileStore getDataFileStore() { * @throws gov.nasa.worldwind.exception.WWRuntimeException * if the Object could not be created * @throws IllegalArgumentException - * if className is null or zero length + */ public static Object createComponent(String className) { if (WWUtil.isEmpty(className)) { diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/WorldWindow.java b/WorldWindAndroid/src/gov/nasa/worldwind/WorldWindow.java index f7c1e73..2f29f7f 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/WorldWindow.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/WorldWindow.java @@ -5,10 +5,16 @@ */ package gov.nasa.worldwind; +import android.content.Context; import gov.nasa.worldwind.cache.GpuResourceCache; import gov.nasa.worldwind.event.*; import gov.nasa.worldwind.geom.Position; import gov.nasa.worldwind.pick.PickedObjectList; +import gov.nasa.worldwind.util.PerformanceStatistic; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; /** * @author dcollins @@ -16,6 +22,8 @@ */ public interface WorldWindow extends WWObject { + public Context getContext(); + /** * Returns the window's current model. * @@ -107,6 +115,36 @@ public interface WorldWindow extends WWObject */ void removeRenderingListener(RenderingListener listener); + /** + * Adds a select listener to this world window. Select listeners are called when a selection is made by the user in + * the world window. A selection is any operation that identifies a visible item. + * + * @param listener The select listener to add. + */ + void addSelectListener(SelectListener listener); + + /** + * Removes the specified select listener associated with this world window. + * + * @param listener The select listener to remove. + */ + void removeSelectListener(SelectListener listener); + + /** + * Adds a position listener to this world window. Position listeners are called when the cursor's position changes. + * They identify the position of the cursor on the globe, or that the cursor is not on the globe. + * + * @param listener The position listener to add. + */ + void addPositionListener(PositionListener listener); + + /** + * Removes the specified position listener associated with this world window. + * + * @param listener The listener to remove. + */ + void removePositionListener(PositionListener listener); + /** * Returns the current latitude, longitude and altitude of the current cursor position, or null if the * cursor is not on the globe. @@ -135,4 +173,24 @@ public interface WorldWindow extends WWObject void redraw(); void invokeInRenderingThread(Runnable r); + + /** + * Activates the per-frame performance statistic specified. Per-frame statistics measure values within a single + * frame of rendering, such as number of tiles drawn to produce the frame. + * + * @param keys The statistics to activate. + */ + void setPerFrameStatisticsKeys(Set keys); + + /** + * Returns the active per-frame performance statistics such as number of tiles drawn in the most recent frame. + * + * @return The keys and values of the active per-frame statistics. + */ + Map getPerFrameStatistics(); // TODO: move the constants from AVKey to this interface. + + public void onSurfaceDestroyed(); + + public void onPause(); + public void onResume(); } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/WorldWindowGLSurfaceView.java b/WorldWindAndroid/src/gov/nasa/worldwind/WorldWindowGLSurfaceView.java index 42b8ada..1270330 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/WorldWindowGLSurfaceView.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/WorldWindowGLSurfaceView.java @@ -17,6 +17,7 @@ import gov.nasa.worldwind.geom.Position; import gov.nasa.worldwind.pick.*; import gov.nasa.worldwind.util.Logging; +import gov.nasa.worldwind.util.PerformanceStatistic; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; @@ -34,10 +35,10 @@ public class WorldWindowGLSurfaceView extends GLSurfaceView implements GLSurface protected InputHandler inputHandler; protected GpuResourceCache gpuResourceCache; protected List renderingListeners = new ArrayList(); + protected List selectListeners = new ArrayList(); + protected List positionListeners = new ArrayList(); protected int viewportWidth; protected int viewportHeight; - protected TextView latitudeText; - protected TextView longitudeText; public WorldWindowGLSurfaceView(Context context) { @@ -145,7 +146,8 @@ public void onSurfaceChanged(GL10 glUnused, int width, int height) // Set the viewport each time the surface size changes. The SceneController and View automatically adapt to the // current viewport dimensions each frame. - GLES20.glViewport(0, 0, width, height); + GLES20.glViewport(0, 0, width, height); + WorldWindowImpl.glCheckError("glViewport"); this.viewportWidth = width; this.viewportHeight = height; } @@ -187,13 +189,18 @@ protected void drawFrame() return; } + Position positionAtStart = this.getCurrentPosition(); + PickedObject selectionAtStart = this.getCurrentSelection(); +// PickedObjectList boxSelectionAtStart = this.getCurrentBoxSelection(); + // Calls to rendering listeners are wrapped in a try/catch block to prevent any exception thrown by a listener // from terminating this frame. this.sendRenderingEvent(this.beforeRenderingEvent); try { - this.sceneController.drawFrame(this.viewportWidth, this.viewportHeight); + this.sceneController.drawFrame(0, this.viewportWidth, this.viewportHeight); + } catch (Exception e) { @@ -203,6 +210,44 @@ protected void drawFrame() // Calls to rendering listeners are wrapped in a try/catch block to prevent any exception thrown by a listener // from terminating this frame. this.sendRenderingEvent(this.afterRenderingEvent); + + // Position and selection notification occurs only on triggering conditions, not same-state conditions: + // start == null, end == null: nothing selected -- don't notify + // start == null, end != null: something now selected -- notify + // start != null, end == null: something was selected but no longer is -- notify + // start != null, end != null, start != end: something new was selected -- notify + // start != null, end != null, start == end: same thing is selected -- don't notify + + Position positionAtEnd = this.getCurrentPosition(); + if (positionAtStart != null || positionAtEnd != null) + { + // call the listener if both are not null or positions are the same + if (positionAtStart != null && positionAtEnd != null) + { + if (!positionAtStart.equals(positionAtEnd)) + this.sendPositionEvent(new PositionEvent(this, sceneController.getPickPoint(), + positionAtStart, positionAtEnd)); + } + else + { + this.sendPositionEvent(new PositionEvent(this, sceneController.getPickPoint(), + positionAtStart, positionAtEnd)); + } + } + + PickedObject selectionAtEnd = this.getCurrentSelection(); + if (selectionAtStart != null || selectionAtEnd != null) + { + this.sendSelectEvent(new SelectEvent(this, SelectEvent.ROLLOVER, + sceneController.getPickPoint(), sceneController.getObjectsAtPickPoint())); + } + +// PickedObjectList boxSelectionAtEnd = this.getCurrentBoxSelection(); +// if (boxSelectionAtStart != null || boxSelectionAtEnd != null) +// { +// this.sendSelectEvent(new SelectEvent(this.drawable, SelectEvent.BOX_ROLLOVER, +// sc.getPickRectangle(), sc.getObjectsInPickRectangle())); +// } } /** {@inheritDoc} */ @@ -274,27 +319,7 @@ public void setInputHandler(InputHandler inputHandler) this.inputHandler.setEventSource(this); } - public TextView getLatitudeText() - { - return this.latitudeText; - } - - public void setLatitudeText(TextView latView) - { - this.latitudeText = latView; - } - - public TextView getLongitudeText() - { - return this.longitudeText; - } - - public void setLongitudeText(TextView lonView) - { - this.longitudeText = lonView; - } - - /** {@inheritDoc} */ + /** {@inheritDoc} */ public GpuResourceCache getGpuResourceCache() { return this.gpuResourceCache; @@ -357,6 +382,68 @@ protected void sendRenderingEvent(RenderingEvent event) } } + public void addSelectListener(SelectListener listener) + { + if (listener == null) + { + String msg = Logging.getMessage("nullValue.ListenerIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + this.selectListeners.add(listener); + this.getInputHandler().addSelectListener(listener); + } + + public void removeSelectListener(SelectListener listener) + { + if (listener == null) + { + String msg = Logging.getMessage("nullValue.ListenerIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + this.selectListeners.remove(listener); + this.getInputHandler().removeSelectListener(listener); + } + + protected void sendSelectEvent(SelectEvent event) + { + for(SelectListener l : selectListeners) { + l.selected(event); + } + } + + public void addPositionListener(PositionListener listener) + { + if (listener == null) + { + String msg = Logging.getMessage("nullValue.ListenerIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + this.positionListeners.add(listener); + } + + public void removePositionListener(PositionListener listener) + { + if (listener == null) + { + String msg = Logging.getMessage("nullValue.ListenerIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + this.positionListeners.remove(listener); + } + + protected void sendPositionEvent(PositionEvent event) + { + for(PositionListener l : positionListeners) { + l.moved(event); + } + } + /** {@inheritDoc} */ public Position getCurrentPosition() { @@ -381,6 +468,28 @@ public PickedObjectList getObjectsAtCurrentPosition() return this.sceneController != null ? this.sceneController.getObjectsAtPickPoint() : null; } + protected PickedObject getCurrentSelection() + { + if (this.sceneController == null) + return null; + + PickedObjectList pol = getObjectsAtCurrentPosition(); + if (pol == null || pol.size() < 1) + return null; + + PickedObject top = pol.getTopPickedObject(); + return top.isTerrain() ? null : top; + } + +// protected PickedObjectList getCurrentBoxSelection() +// { +// if (this.sceneController == null) +// return null; +// +// PickedObjectList pol = this.sceneController.getObjectsInPickRectangle(); +// return pol != null && pol.size() > 0 ? pol : null; +// } + /** {@inheritDoc} */ public void redraw() { @@ -503,4 +612,23 @@ public void onMessage(Message message) { // Empty implementation } + + public void setPerFrameStatisticsKeys(Set keys) + { + if (this.sceneController != null) + this.sceneController.setPerFrameStatisticsKeys(keys); + } + + public Map getPerFrameStatistics() + { + if (this.sceneController == null || this.sceneController.getPerFrameStatistics() == null) + return new HashMap(0); + + return this.sceneController.getPerFrameStatistics(); + } + + @Override + public void onSurfaceDestroyed() { + + } } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/WorldWindowGLTextureView.java b/WorldWindAndroid/src/gov/nasa/worldwind/WorldWindowGLTextureView.java new file mode 100644 index 0000000..e80beaa --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/WorldWindowGLTextureView.java @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind; + +import android.content.Context; +import android.opengl.GLSurfaceView; +import android.util.AttributeSet; +import gov.nasa.worldwind.avlist.AVList; +import gov.nasa.worldwind.avlist.AVListImpl; +import gov.nasa.worldwind.cache.GpuResourceCache; +import gov.nasa.worldwind.event.*; +import gov.nasa.worldwind.exception.WWRuntimeException; +import gov.nasa.worldwind.geom.Position; +import gov.nasa.worldwind.pick.PickedObject; +import gov.nasa.worldwind.pick.PickedObjectList; +import gov.nasa.worldwind.util.GLTextureView; +import gov.nasa.worldwind.util.Logging; +import gov.nasa.worldwind.util.PerformanceStatistic; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * @author dcollins + * @version $Id: WorldWindowGLSurfaceView.java 831 2012-10-08 20:51:39Z tgaskins $ + */ +public class WorldWindowGLTextureView extends GLTextureView implements WorldWindow +{ + private WorldWindowImpl wwo; + + public WorldWindowGLTextureView(Context context) + { + this(context, null, null); + } + + public WorldWindowGLTextureView(Context context, AttributeSet attrs) + { + this(context, attrs, null); + } + + public WorldWindowGLTextureView(Context context, AttributeSet attrs, EGLConfigChooser configChooser) + { + super(context, attrs); + + try + { + this.setEGLContextClientVersion(2); // Specify that this view requires an OpenGL ES 2.0 compatible context. + + if (configChooser != null) + this.setEGLConfigChooser(configChooser); + else + this.setEGLConfigChooser(8, 8, 8, 8, 16, 0); // RGBA8888, 16-bit depth buffer, no stencil buffer. + + wwo = new WorldWindowImpl(getContext(), this); + setOnTouchListener(wwo); + this.setRenderer(wwo); + this.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); // Must be called after setRenderer. + } + catch (Exception e) + { + String msg = Logging.getMessage("WorldWindow.UnableToCreateWorldWindow"); + Logging.error(msg); + throw new WWRuntimeException(msg, e); + } + } + + public void setFrameRate(double frameRate) { + wwo.setFrameRate(frameRate); + } + + public PickedObject getCurrentSelection() { + return wwo.getCurrentSelection(); + } + + public GpuResourceCache createGpuResourceCache() { + return wwo.createGpuResourceCache(); + } + + public double getFrameRate() { + return wwo.getFrameRate(); + } + + public void setFrameRate(int frameRate) { + wwo.setFrameRate(frameRate); + } + + public InputHandler createInputHandler() { + return wwo.createInputHandler(); + } + + public View createView() { + return wwo.createView(); + } + + public SceneController createSceneController() { + return wwo.createSceneController(); + } + + public void setGpuResourceCache(GpuResourceCache cache) { + wwo.setGpuResourceCache(cache); + } + + public static Long getLongValue(AVList avList, String key) { + return AVListImpl.getLongValue(avList, key); + } + + @Override + public void firePropertyChange(PropertyChangeEvent event) { + wwo.firePropertyChange(event); + } + + @Override + public void firePropertyChange(String propertyName, Object oldValue, Object newValue) { + wwo.firePropertyChange(propertyName, oldValue, newValue); + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener listener) { + wwo.removePropertyChangeListener(listener); + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + wwo.addPropertyChangeListener(listener); + } + + @Override + public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) { + wwo.removePropertyChangeListener(propertyName, listener); + } + + @Override + public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) { + wwo.addPropertyChangeListener(propertyName, listener); + } + + @Override + public AVList clearList() { + return wwo.clearList(); + } + + @Override + public AVList copy() { + return wwo.copy(); + } + + @Override + public Object removeKey(String key) { + return wwo.removeKey(key); + } + + @Override + public boolean hasKey(String key) { + return wwo.hasKey(key); + } + + @Override + public Set> getEntries() { + return wwo.getEntries(); + } + + @Override + public AVList setValues(AVList list) { + return wwo.setValues(list); + } + + @Override + public Collection getValues() { + return wwo.getValues(); + } + + @Override + public Object setValue(String key, Object value) { + return wwo.setValue(key, value); + } + + @Override + public String getStringValue(String key) { + return wwo.getStringValue(key); + } + + @Override + public Object getValue(String key) { + return wwo.getValue(key); + } + + @Override + public void onMessage(Message message) { + wwo.onMessage(message); + } + + @Override + public void propertyChange(PropertyChangeEvent event) { + wwo.propertyChange(event); + } + + @Override + public Map getPerFrameStatistics() { + return wwo.getPerFrameStatistics(); + } + + @Override + public void setPerFrameStatisticsKeys(Set keys) { + wwo.setPerFrameStatisticsKeys(keys); + } + + @Override + public void redraw() { + wwo.redraw(); + } + + @Override + public void removePositionListener(PositionListener listener) { + wwo.removePositionListener(listener); + } + + @Override + public void addPositionListener(PositionListener listener) { + wwo.addPositionListener(listener); + } + + @Override + public PickedObjectList getObjectsAtCurrentPosition() { + return wwo.getObjectsAtCurrentPosition(); + } + + @Override + public Position getCurrentPosition() { + return wwo.getCurrentPosition(); + } + + @Override + public void removeSelectListener(SelectListener listener) { + wwo.removeSelectListener(listener); + } + + @Override + public void addSelectListener(SelectListener listener) { + wwo.addSelectListener(listener); + } + + @Override + public void removeRenderingListener(RenderingListener listener) { + wwo.removeRenderingListener(listener); + } + + @Override + public void addRenderingListener(RenderingListener listener) { + wwo.addRenderingListener(listener); + } + + @Override + public Model getModel() { + return wwo.getModel(); + } + + @Override + public void setModel(Model model) { + wwo.setModel(model); + } + + @Override + public View getView() { + return wwo.getView(); + } + + @Override + public void setView(View view) { + wwo.setView(view); + } + + @Override + public SceneController getSceneController() { + return wwo.getSceneController(); + } + + @Override + public void setSceneController(SceneController sceneController) { + wwo.setSceneController(sceneController); + } + + @Override + public InputHandler getInputHandler() { + return wwo.getInputHandler(); + } + + @Override + public void setInputHandler(InputHandler inputHandler) { + wwo.setInputHandler(inputHandler); + } + + @Override + public GpuResourceCache getGpuResourceCache() { + return wwo.getGpuResourceCache(); + } + + @Override + public void onResume() { + super.onResume(); + wwo.onResume(); + } + + @Override + public void onPause() { + super.onPause(); + wwo.onPause(); + } + + @Override + public void onSurfaceDestroyed() { + wwo.onSurfaceDestroyed(); + } + + @Override + public void invokeInRenderingThread(Runnable runnable) { + wwo.invokeInRenderingThread(runnable); + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/WorldWindowImpl.java b/WorldWindAndroid/src/gov/nasa/worldwind/WorldWindowImpl.java new file mode 100644 index 0000000..b22f310 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/WorldWindowImpl.java @@ -0,0 +1,599 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ + +package gov.nasa.worldwind; + +import android.content.Context; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import android.opengl.GLU; +import android.opengl.GLUtils; +import android.os.SystemClock; +import android.view.MotionEvent; +import gov.nasa.worldwind.avlist.AVKey; +import gov.nasa.worldwind.cache.BasicGpuResourceCache; +import gov.nasa.worldwind.cache.GpuResourceCache; +import gov.nasa.worldwind.event.*; +import gov.nasa.worldwind.geom.Position; +import gov.nasa.worldwind.pick.PickedObject; +import gov.nasa.worldwind.pick.PickedObjectList; +import gov.nasa.worldwind.util.GLTextureView; +import gov.nasa.worldwind.util.Logging; +import gov.nasa.worldwind.util.PerformanceStatistic; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; +import java.lang.ref.WeakReference; +import java.util.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * WorldWindow implementation. Manages rendering thread for automatic rendering. + * + * Created by kedzie on 4/23/14. + */ +public class WorldWindowImpl extends WWObjectImpl implements WorldWindow, GLSurfaceView.Renderer, android.view.View.OnTouchListener { + + public static boolean DEBUG = true; + + protected ScheduledExecutorService mTimer; //Timer used to schedule drawing + + private WeakReference mContext; + protected SceneController sceneController; + protected InputHandler inputHandler; + protected GpuResourceCache gpuResourceCache; + protected List renderingListeners = new ArrayList(); + protected List selectListeners = new ArrayList(); + protected List positionListeners = new ArrayList(); + protected int viewportWidth; + protected int viewportHeight; + + protected double mFrameRate; //Target frame rate to render at + protected int mFrameCount; //Used for determining FPS + private long mStartTime = System.nanoTime(); //Used for determining FPS + protected double mLastMeasuredFPS; //Last measured FPS value + protected long mLastMeasuredFrameTime; + private long mLastRender; //Time of last rendering. Used for animation delta time + protected int mLastReportedGLError = 0; // Keep track of the last reported OpenGL error + + private GLTextureView textureView; + + public WorldWindowImpl(Context context, GLTextureView view) { + mContext = new WeakReference(context); + this.textureView = view; + + // Create the SceneController and assign its View before attaching it to this WorldWindow. We do this to avoid + // receiving property change events from the SceneController before the superclass GLSurfaceView is properly + // initialized. + SceneController sc = this.createSceneController(); + if (sc != null) { + sc.setView(this.createView()); + } + this.setSceneController(sc); + this.setInputHandler(this.createInputHandler()); + this.setGpuResourceCache(this.createGpuResourceCache()); + } + + @Override + public Context getContext() { + return mContext.get(); + } + + @Override + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + // Clear the GPU resource cache each time the surface is created or recreated. This happens when the rendering + // thread starts or when the EGL context is lost. All GPU object names are invalid, and must be recreated. Since + // the EGL context has changed, the currently active context is not the one used to create the Gpu resources in + // the cache. The cache is emptied and the GL silently ignores deletion of resource names that it does not + // recognize. + if (this.gpuResourceCache != null) + this.gpuResourceCache.clear(); + } + + @Override + public void onSurfaceDestroyed() { + stopRendering(); + } + + @Override + public void onPause() { + stopRendering(); + } + + @Override + public void onResume() { + startRendering(); + } + + @Override + public void onSurfaceChanged(GL10 gl, int width, int height) { + GLES20.glViewport(0, 0, width, height); + glCheckError("glViewport"); + this.viewportWidth = width; + this.viewportHeight = height; + startRendering(); + } + + @Override + public void onDrawFrame(GL10 gl) { + final double deltaTime = (SystemClock.elapsedRealtime() - mLastRender) / 1000d; + mLastRender = SystemClock.elapsedRealtime(); + + drawFrame(deltaTime); + + ++mFrameCount; + if (mFrameCount % 50 == 0) { + long now = System.nanoTime(); + double elapsedS = (now - mStartTime) / 1.0e9; + double msPerFrame = (1000 * elapsedS / mFrameCount); + mLastMeasuredFPS = 1000 / msPerFrame; + + mFrameCount = 0; + mStartTime = now; + + getPerFrameStatistics().put(PerformanceStatistic.FRAME_RATE, new PerformanceStatistic(PerformanceStatistic.FRAME_RATE, "Frame Rate (fps)", (int) this.mLastMeasuredFPS)); + firePropertyChange(PerformanceStatistic.FRAME_RATE, 0, mLastMeasuredFPS); + } + + getPerFrameStatistics().put(PerformanceStatistic.FRAME_TIME, new PerformanceStatistic(PerformanceStatistic.FRAME_TIME, "Frame Time (ms)", (int) this.mLastMeasuredFrameTime)); + + int error = GLES20.glGetError(); + + if(error > 0) + { + if(error != mLastReportedGLError) + { + mLastReportedGLError = error; + throw new RuntimeException("OpenGL Error: " + GLU.gluErrorString(error) + " " + error); + } + } else { + mLastReportedGLError = 0; + } + } + + protected void drawFrame(double deltaTime) + { + if (this.sceneController == null) + { + Logging.error(Logging.getMessage("WorldWindow.ScnCntrllerNullOnRepaint")); + return; + } + + Position positionAtStart = this.getCurrentPosition(); + PickedObject selectionAtStart = this.getCurrentSelection(); +// PickedObjectList boxSelectionAtStart = this.getCurrentBoxSelection(); + + // Calls to rendering listeners are wrapped in a try/catch block to prevent any exception thrown by a listener + // from terminating this frame. + this.sendRenderingEvent(this.beforeRenderingEvent); + + try + { + this.sceneController.drawFrame(deltaTime, this.viewportWidth, this.viewportHeight); + + this.setValue(PerformanceStatistic.FRAME_TIME, mLastMeasuredFrameTime); + this.setValue(PerformanceStatistic.FRAME_RATE, mLastMeasuredFPS); + } + catch (Exception e) + { + Logging.error(Logging.getMessage("WorldWindow.ExceptionDrawingWorldWindow"), e); + } + + // Calls to rendering listeners are wrapped in a try/catch block to prevent any exception thrown by a listener + // from terminating this frame. + this.sendRenderingEvent(this.afterRenderingEvent); + + // Position and selection notification occurs only on triggering conditions, not same-state conditions: + // start == null, end == null: nothing selected -- don't notify + // start == null, end != null: something now selected -- notify + // start != null, end == null: something was selected but no longer is -- notify + // start != null, end != null, start != end: something new was selected -- notify + // start != null, end != null, start == end: same thing is selected -- don't notify + + Position positionAtEnd = this.getCurrentPosition(); + if (positionAtStart != null || positionAtEnd != null) + { + if (positionAtStart != null && positionAtEnd != null) + { + if (!positionAtStart.equals(positionAtEnd)) + this.sendPositionEvent(new PositionEvent(this, sceneController.getPickPoint(), + positionAtStart, positionAtEnd)); + } + else + { + this.sendPositionEvent(new PositionEvent(this, sceneController.getPickPoint(), + positionAtStart, positionAtEnd)); + } + } + + PickedObject selectionAtEnd = this.getCurrentSelection(); + if (selectionAtStart != null || selectionAtEnd != null) + { + this.sendSelectEvent(new SelectEvent(this, SelectEvent.ROLLOVER, + sceneController.getPickPoint(), sceneController.getObjectsAtPickPoint())); + } + +// PickedObjectList boxSelectionAtEnd = this.getCurrentBoxSelection(); +// if (boxSelectionAtStart != null || boxSelectionAtEnd != null) +// { +// this.sendSelectEvent(new SelectEvent(this.drawable, SelectEvent.BOX_ROLLOVER, +// sc.getPickRectangle(), sc.getObjectsInPickRectangle())); +// } + } + + public static void glCheckError(String op) { + if(!DEBUG) return; + int error; + while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { + final StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + StringBuilder sb = new StringBuilder(stackTrace.length*80); + for(StackTraceElement element : stackTrace) { + sb.append("\n").append(element.toString()); + } + Logging.error(op + ": glError " + GLUtils.getEGLErrorString(error) + sb.toString()); +// throw new RuntimeException(op + ": glError " + GLUtils.getEGLErrorString(error)); + } + } + + protected final RenderingEvent beforeRenderingEvent = new RenderingEvent(this, RenderingEvent.BEFORE_RENDERING); + protected final RenderingEvent afterRenderingEvent = new RenderingEvent(this, RenderingEvent.AFTER_RENDERING); + + @Override + public Model getModel() + { + return this.sceneController != null ? this.sceneController.getModel() : null; + } + + @Override + public void setModel(Model model) + { + // model can be null, that's ok - it indicates no model. + if (this.sceneController != null) + this.sceneController.setModel(model); + } + + @Override + public View getView() + { + return this.sceneController != null ? this.sceneController.getView() : null; + } + + @Override + public void setView(View view) + { + // view can be null, that's ok - it indicates no view. + if (this.sceneController != null) + this.sceneController.setView(view); + } + + @Override + public SceneController getSceneController() + { + return this.sceneController; + } + + @Override + public void setSceneController(SceneController sceneController) + { + if (this.sceneController != null) + { + this.sceneController.removePropertyChangeListener(this); + this.sceneController.setGpuResourceCache(null); + } + + if (sceneController != null) + { + sceneController.addPropertyChangeListener(this); + sceneController.setGpuResourceCache(this.gpuResourceCache); + } + + this.sceneController = sceneController; + } + + @Override + public InputHandler getInputHandler() + { + return this.inputHandler; + } + + @Override + public void setInputHandler(InputHandler inputHandler) + { + if (this.inputHandler != null) + this.inputHandler.setEventSource(null); + + // Fall back to a no-op input handler if the caller specifies null. + this.inputHandler = inputHandler != null ? inputHandler : new NoOpInputHandler(); + + // Configure this world window as the input handler's event source. + this.inputHandler.setEventSource(this); + } + + @Override + public GpuResourceCache getGpuResourceCache() + { + return this.gpuResourceCache; + } + + public void setGpuResourceCache(GpuResourceCache cache) + { + this.gpuResourceCache = cache; + + if (this.sceneController != null) + this.sceneController.setGpuResourceCache(cache); + } + + @Override + public void addRenderingListener(RenderingListener listener) + { + if (listener == null) + { + String msg = Logging.getMessage("nullValue.ListenerIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + this.renderingListeners.add(listener); + } + + @Override + public void removeRenderingListener(RenderingListener listener) + { + if (listener == null) + { + String msg = Logging.getMessage("nullValue.ListenerIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + this.renderingListeners.remove(listener); + } + + protected void sendRenderingEvent(RenderingEvent event) + { + if (this.renderingListeners.isEmpty()) + return; + + for (int i = 0; i < this.renderingListeners.size(); i++) + { + RenderingListener listener = this.renderingListeners.get(i); + try + { + // This method is called during rendering, so we wrao each rendering listener call in a try/catch block + // to prevent exceptions thrown by rendering listeners from terminating the current frame. This also + // ensures that an exception thrown by one listener does not prevent the others from receiving the + // event. + listener.stageChanged(event); + } + catch (Exception e) + { + Logging.error(Logging.getMessage("generic.ExceptionSendingEvent", event, listener), e); + } + } + } + + @Override + public void addSelectListener(SelectListener listener) + { + if (listener == null) + { + String msg = Logging.getMessage("nullValue.ListenerIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + this.selectListeners.add(listener); + this.getInputHandler().addSelectListener(listener); + } + + @Override + public void removeSelectListener(SelectListener listener) + { + if (listener == null) + { + String msg = Logging.getMessage("nullValue.ListenerIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + this.selectListeners.remove(listener); + this.getInputHandler().removeSelectListener(listener); + } + + protected void sendSelectEvent(SelectEvent event) + { + for(SelectListener l : selectListeners) { + l.selected(event); + } + } + + @Override + public void addPositionListener(PositionListener listener) + { + if (listener == null) + { + String msg = Logging.getMessage("nullValue.ListenerIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + this.positionListeners.add(listener); + } + + @Override + public void removePositionListener(PositionListener listener) + { + if (listener == null) + { + String msg = Logging.getMessage("nullValue.ListenerIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + this.positionListeners.remove(listener); + } + + protected void sendPositionEvent(PositionEvent event) + { + for(PositionListener l : positionListeners) { + l.moved(event); + } + } + + @Override + public Position getCurrentPosition() + { + PickedObjectList pol = this.getObjectsAtCurrentPosition(); + if (pol == null || pol.isEmpty()) + return null; + + PickedObject po = pol.getTopPickedObject(); + if (po != null && po.hasPosition()) + return po.getPosition(); + + po = pol.getTerrainObject(); + if (po != null) + return po.getPosition(); + + return null; + } + + @Override + public PickedObjectList getObjectsAtCurrentPosition() + { + return this.sceneController != null ? this.sceneController.getObjectsAtPickPoint() : null; + } + + protected PickedObject getCurrentSelection() + { + if (this.sceneController == null) + return null; + + PickedObjectList pol = getObjectsAtCurrentPosition(); + if (pol == null || pol.size() < 1) + return null; + + PickedObject top = pol.getTopPickedObject(); + return top.isTerrain() ? null : top; + } + + @Override + public boolean onTouch(android.view.View v, MotionEvent event) { + // Let the InputHandler process the touch event first. If it returns true indicating that it handled the event, + // then we suppress the default functionality. + // noinspection SimplifiableIfStatement + if (this.inputHandler != null && this.inputHandler.onTouch(v, event)) + return true; + return false; + } + +// protected PickedObjectList getCurrentBoxSelection() +// { +// if (this.sceneController == null) +// return null; +// +// PickedObjectList pol = this.sceneController.getObjectsInPickRectangle(); +// return pol != null && pol.size() > 0 ? pol : null; +// } + + private class RequestRenderTask implements Runnable { + public void run() { + if (textureView != null) { + textureView.requestRender(); + } + } + } + + public void startRendering() { + mLastRender = SystemClock.elapsedRealtime(); + + if (mTimer!=null || mFrameRate==0) {return;} + + mTimer = Executors.newScheduledThreadPool(1); + mTimer.scheduleAtFixedRate(new RequestRenderTask(), 0, (long) (1000 / mFrameRate), TimeUnit.MILLISECONDS); + } + + /** + * Stop rendering the scene. + * + * @return true if rendering was stopped, false if rendering was already + * stopped (no action taken) + */ + protected boolean stopRendering() { + if (mTimer != null) { + mTimer.shutdownNow(); + mTimer = null; + return true; + } + return false; + } + + public double getFrameRate() { + return mFrameRate; + } + + public void setFrameRate(double frameRate) { + this.mFrameRate = frameRate; + if (stopRendering()) { + // Restart timer with new frequency + startRendering(); + } + } + + @Override + public void redraw() + { + if(mFrameRate==0) + textureView.requestRender(); + } + + public void setFrameRate(int frameRate) { + setFrameRate((double) frameRate); + } + + @Override + public void invokeInRenderingThread(Runnable runnable) + { + textureView.queueEvent(runnable); + } + + protected SceneController createSceneController() + { + return (SceneController) WorldWind.createConfigurationComponent(AVKey.SCENE_CONTROLLER_CLASS_NAME); + } + + protected InputHandler createInputHandler() + { + return (InputHandler) WorldWind.createConfigurationComponent(AVKey.INPUT_HANDLER_CLASS_NAME); + } + + protected View createView() + { + return (View) WorldWind.createConfigurationComponent(AVKey.VIEW_CLASS_NAME); + } + + protected GpuResourceCache createGpuResourceCache() + { + long size = Configuration.getLongValue(AVKey.GPU_RESOURCE_CACHE_SIZE); + return new BasicGpuResourceCache((long) (0.8 * size), size); + } + + @Override + public void setPerFrameStatisticsKeys(Set keys) + { + if (this.sceneController != null) + this.sceneController.setPerFrameStatisticsKeys(keys); + } + + @Override + public Map getPerFrameStatistics() + { + if (this.sceneController == null || this.sceneController.getPerFrameStatistics() == null) + return new HashMap(0); + + return this.sceneController.getPerFrameStatistics(); + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/animation/AngleEvaluator.java b/WorldWindAndroid/src/gov/nasa/worldwind/animation/AngleEvaluator.java new file mode 100644 index 0000000..c5d02b3 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/animation/AngleEvaluator.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ + +package gov.nasa.worldwind.animation; + +import android.animation.TypeEvaluator; +import gov.nasa.worldwind.geom.Angle; + +/** + * {@link android.animation.TypeEvaluator} for {@link gov.nasa.worldwind.geom.Angle} + * + * Created by kedzie on 3/28/14. + * @author Mark Kedzierski + */ +public class AngleEvaluator implements TypeEvaluator{ + + @Override + public Angle evaluate(float fraction, Angle startValue, Angle endValue) { + return Angle.mix(fraction, startValue, endValue); + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/animation/DoubleEvaluator.java b/WorldWindAndroid/src/gov/nasa/worldwind/animation/DoubleEvaluator.java new file mode 100644 index 0000000..591fa51 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/animation/DoubleEvaluator.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ + +package gov.nasa.worldwind.animation; + +import android.animation.TypeEvaluator; +import gov.nasa.worldwind.geom.Position; + +/** + * {@link android.animation.TypeEvaluator} for {@link java.lang.Double} + * + * Created by kedzie on 3/28/14. + * @author Mark Kedzierski + */ +public class DoubleEvaluator implements TypeEvaluator { + + @Override + public Double evaluate(float fraction, Double startValue, Double endValue) { + return startValue + (endValue-startValue)*fraction; + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/animation/PositionEvaluator.java b/WorldWindAndroid/src/gov/nasa/worldwind/animation/PositionEvaluator.java new file mode 100644 index 0000000..d7d69d5 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/animation/PositionEvaluator.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ + +package gov.nasa.worldwind.animation; + +import android.animation.TypeEvaluator; +import gov.nasa.worldwind.geom.Position; + +/** + * {@link android.animation.TypeEvaluator} for {@link gov.nasa.worldwind.geom.Position} + * + * Created by kedzie on 3/28/14. + * @author Mark Kedzierski + */ +public class PositionEvaluator implements TypeEvaluator { + + @Override + public Position evaluate(float fraction, Position startValue, Position endValue) { + return Position.interpolateGreatCircle(fraction, startValue, endValue); + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/avlist/AVKey.java b/WorldWindAndroid/src/gov/nasa/worldwind/avlist/AVKey.java index 4288c51..8655cfe 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/avlist/AVKey.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/avlist/AVKey.java @@ -116,6 +116,7 @@ public interface AVKey { final String LINEAR = "gov.nasa.worldwind.avkey.Linear"; final String LITTLE_ENDIAN = "gov.nasa.worldwind.avkey.LittleEndian"; final String LOGCAT_TAG = "gov.nasa.worldwind.avkey.LogcatTag"; + final String LOXODROME = "gov.nasa.worldwind.avkey.Loxodrome"; final String MAP_SCALE = "gov.nasa.worldwind.avkey.MapScale"; /** @@ -149,6 +150,8 @@ public interface AVKey { final String NETWORK_RETRIEVAL_ENABLED = "gov.nasa.worldwind.avkey.NetworkRetrievalEnabled"; final String NETWORK_STATUS_CLASS_NAME = "gov.nasa.worldwind.avkey.NetworkStatusClassName"; final String NETWORK_STATUS_TEST_SITES = "gov.nasa.worldwind.avkey.NetworkStatusTestSites"; + final String NORTH = "gov.nasa.worldwind.avkey.North"; + final String NUM_EMPTY_LEVELS = "gov.nasa.worldwind.avkey.NumEmptyLevels"; final String NUM_LEVELS = "gov.nasa.worldwind.avkey.NumLevels"; @@ -200,7 +203,9 @@ public interface AVKey { final String SERVICE_NAME = "gov.nasa.worldwind.avkey.ServiceName"; final String SESSION_CACHE_CLASS_NAME = "gov.nasa.worldwind.avkey.SessionCacheClassName"; final String SHORT_DESCRIPTION = "gov.nasa.worldwind.avkey.Server.ShortDescription"; + final String SOUTH = "gov.nasa.worldwdind.avkey.South"; final String STYLE_NAMES = "gov.nasa.worldwind.avkey.StyleNames"; + final String SURFACE_TILE_DRAW_CONTEXT = "gov.nasa.worldwind.avkey.SurfaceTileDrawContext"; final String TESSELLATOR_FACTORY = "gov.nasa.worldwind.avkey.TessellatorFactory"; final String TESSELLATOR_CONFIG_FILE = "gov.nasa.worldwind.avkey.TessellatorConfigFile"; @@ -223,6 +228,7 @@ public interface AVKey { final String URL_PROXY_PORT = "gov.nasa.worldwind.avkey.UrlProxyPort"; final String URL_PROXY_TYPE = "gov.nasa.worldwind.avkey.UrlProxyType"; final String URL_READ_TIMEOUT = "gov.nasa.worldwind.avkey.URLReadTimeout"; + final String USE_MIP_MAPS = "gov.nasa.worldwind.avkey.UseMipMaps"; final String USE_TRANSPARENT_TEXTURES = "gov.nasa.worldwind.avkey.UseTransparentTextures"; final String VERTICAL_EXAGGERATION = "gov.nasa.worldwind.avkey.VerticalExaggeration"; diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/cache/BasicGpuResourceCache.java b/WorldWindAndroid/src/gov/nasa/worldwind/cache/BasicGpuResourceCache.java index 3a5a56e..7aa1f53 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/cache/BasicGpuResourceCache.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/cache/BasicGpuResourceCache.java @@ -6,12 +6,13 @@ package gov.nasa.worldwind.cache; +import android.opengl.GLES20; import gov.nasa.worldwind.Disposable; +import gov.nasa.worldwind.WorldWindowImpl; import gov.nasa.worldwind.render.GpuProgram; import gov.nasa.worldwind.render.GpuTexture; import gov.nasa.worldwind.util.Logging; import gov.nasa.worldwind.util.WWUtil; -import android.opengl.GLES20; /** * Provides the interface for caching of OpenGL resources that are stored on or registered with a GL context. This cache @@ -22,7 +23,7 @@ * etc. -- and there is a current Open GL context, the appropriate glDelete function is called to de-register the resource with the GPU. If there is no current * OpenGL context the resource is not deleted and will likely remain allocated on the GPU until the GL context is destroyed. Edited By: Nicola Dorigatti, * Trilogis - * + * * @author nicola.dorigatti Trilogis SRL * @version $Id: BasicGpuResourceCache.java 733 2012-09-02 17:15:09Z dcollins $ */ @@ -71,7 +72,7 @@ protected void onEntryRemoved(Object key, Object clientObject) { // case, the cache is emptied and the GL silently ignores deletion of resource names that it does not recognize. if (!(clientObject instanceof CacheEntry)) // shouldn't be null or wrong type, but check anyway - return; + return; CacheEntry entry = (CacheEntry) clientObject; @@ -81,6 +82,7 @@ protected void onEntryRemoved(Object key, Object clientObject) { } else if (entry.resourceType.equals(VBO_BUFFERS)) { int[] ids = (int[]) entry.resource; GLES20.glDeleteBuffers(ids.length, ids, 0); + WorldWindowImpl.glCheckError("glDeleteBuffers"); } } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/cache/BasicMemoryCacheSet.java b/WorldWindAndroid/src/gov/nasa/worldwind/cache/BasicMemoryCacheSet.java index 0f52140..fd6bb98 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/cache/BasicMemoryCacheSet.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/cache/BasicMemoryCacheSet.java @@ -6,7 +6,10 @@ package gov.nasa.worldwind.cache; import gov.nasa.worldwind.util.Logging; +import gov.nasa.worldwind.util.PerformanceStatistic; +import java.util.ArrayList; +import java.util.Collection; import java.util.concurrent.ConcurrentHashMap; /** @@ -77,4 +80,17 @@ public synchronized void clear() this.caches.clear(); } + + public Collection getPerformanceStatistics() + { + ArrayList stats = new ArrayList(); + + for (MemoryCache cache : this.caches.values()) + { + stats.add(new PerformanceStatistic(PerformanceStatistic.MEMORY_CACHE, "Cache Size (Kb): " + cache.getName(), + cache.getUsedCapacity() / 1000)); + } + + return stats; + } } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/cache/MemoryCacheSet.java b/WorldWindAndroid/src/gov/nasa/worldwind/cache/MemoryCacheSet.java index 0ecbf86..cc983ef 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/cache/MemoryCacheSet.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/cache/MemoryCacheSet.java @@ -5,6 +5,10 @@ */ package gov.nasa.worldwind.cache; +import gov.nasa.worldwind.util.PerformanceStatistic; + +import java.util.Collection; + /** * @author dcollins * @version $Id: MemoryCacheSet.java 834 2012-10-08 22:25:55Z dcollins $ @@ -18,4 +22,6 @@ public interface MemoryCacheSet boolean contains(String key); void clear(); + + Collection getPerformanceStatistics(); } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/event/BasicInputHandler.java b/WorldWindAndroid/src/gov/nasa/worldwind/event/BasicInputHandler.java index 74703e3..6a5c667 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/event/BasicInputHandler.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/event/BasicInputHandler.java @@ -5,30 +5,37 @@ */ package gov.nasa.worldwind.event; -import android.app.Activity; -import android.content.Context; import android.graphics.Point; -import android.view.*; +import android.os.*; +import android.os.Message; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; import android.view.View; -import android.widget.TextView; -import gov.nasa.worldwind.*; +import gov.nasa.worldwind.BasicView; +import gov.nasa.worldwind.WWObjectImpl; +import gov.nasa.worldwind.WorldWindow; import gov.nasa.worldwind.geom.*; -import gov.nasa.worldwind.globes.Globe; +import gov.nasa.worldwind.pick.PickedObject; +import gov.nasa.worldwind.pick.PickedObjectList; +import gov.nasa.worldwind.util.Logging; + +import java.util.ArrayList; +import java.util.List; /** * @author ccrick * @version $Id: BasicInputHandler.java 852 2012-10-12 19:35:43Z dcollins $ */ -public class BasicInputHandler extends WWObjectImpl implements InputHandler +public class BasicInputHandler extends WWObjectImpl implements InputHandler, ScaleGestureDetector.OnScaleGestureListener { - // TODO: put this value in a configuration file - protected static final int SINGLE_TAP_INTERVAL = 300; - protected static final int DOUBLE_TAP_INTERVAL = 300; protected static final int JUMP_THRESHOLD = 100; protected static final double PINCH_WIDTH_DELTA_THRESHOLD = 5; protected static final double PINCH_ROTATE_DELTA_THRESHOLD = 1; + protected static final double DEFAULT_DRAG_SLOPE_FACTOR = 0.002; protected WorldWindow eventSource; + protected List selectListeners = new ArrayList(); protected float mPreviousX = -1; protected float mPreviousY = -1; @@ -36,21 +43,94 @@ public class BasicInputHandler extends WWObjectImpl implements InputHandler protected float mPreviousX2 = -1; protected float mPreviousY2 = -1; - protected double mPrevPinchWidth = -1; - - protected boolean mIsTap = false; - protected long mLastTap = -1; // system time in ms of last tap + protected float mInitialX=-1; + protected float mInitialY=-1; + protected double dragSlopeFactor = DEFAULT_DRAG_SLOPE_FACTOR; // Temporary properties used to avoid constant allocation when responding to input events. - protected Point screenPoint = new Point(); + protected Point touchPoint = new Point(); + protected Point previousTouchPoint = new Point(); protected Position position = new Position(); protected Vec4 point1 = new Vec4(); protected Vec4 point2 = new Vec4(); - public BasicInputHandler() + protected ScaleGestureDetector scaleGestureDetector; + private GestureDetector gestureDetector; + private Position selectedPosition; + + private static final int WHAT_STOP_ANIMATIONS = 0; + + private Handler animationHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch(msg.what) { + case WHAT_STOP_ANIMATIONS: + ((BasicView) eventSource.getView()).stopAnimations(); + break; + } + } + }; + + public BasicInputHandler() { } + public void addSelectListener(SelectListener listener) + { + this.selectListeners.add(listener); + } + + public void removeSelectListener(SelectListener listener) + { + this.selectListeners.remove(listener); + } + + protected void callSelectListeners(SelectEvent event) + { + for (SelectListener listener : this.selectListeners) + { + listener.selected(event); + } + } + + /** + * Returns the factor that dampens view movement when the user pans drags the cursor in a way that could + * cause an abrupt transition. + * + * @return factor dampening view movement when a mouse drag event would cause an abrupt transition. + * @see #setDragSlopeFactor + */ + public double getDragSlopeFactor() + { + return this.dragSlopeFactor; + } + + /** + * Sets the factor that dampens view movement when a mouse drag event would cause an abrupt + * transition. The drag slope is the ratio of screen pixels to Cartesian distance moved, measured by the previous + * and current mouse points. As drag slope gets larger, it becomes more difficult to operate the view. This + * typically happens while dragging over and around the horizon, where movement of a few pixels can cause the view + * to move many kilometers. This factor is the amount of damping applied to the view movement in such + * cases. Setting factor to zero will disable this behavior, while setting factor to a + * positive value may dampen the effects of mouse dragging. + * + * @param factor dampening view movement when a mouse drag event would cause an abrupt transition. Must be greater + * than or equal to zero. + * + * @throws IllegalArgumentException if factor is less than zero. + */ + public void setDragSlopeFactor(double factor) + { + if (factor < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "factor < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.dragSlopeFactor = factor; + } + public WorldWindow getEventSource() { return this.eventSource; @@ -58,79 +138,83 @@ public WorldWindow getEventSource() public void setEventSource(WorldWindow eventSource) { - this.eventSource = eventSource; + this.eventSource = eventSource; + scaleGestureDetector = new ScaleGestureDetector(eventSource.getContext(), this); + gestureDetector = new GestureDetector(eventSource.getContext(), new GestureDetector.SimpleOnGestureListener() { + + @Override + public void onLongPress(MotionEvent e) { + + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + return false; + } + }); + gestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() { + @Override + public boolean onSingleTapConfirmed(final MotionEvent e) { + return true; + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + + return false; + } + + @Override + public boolean onDoubleTapEvent(MotionEvent e) { + return false; + } + }); } public boolean onTouch(final View view, MotionEvent motionEvent) { - int pointerCount = motionEvent.getPointerCount(); + scaleGestureDetector.onTouchEvent(motionEvent); + gestureDetector.onTouchEvent(motionEvent); + + final int pointerCount = motionEvent.getPointerCount(); final float x = motionEvent.getX(0); final float y = motionEvent.getY(0); + updateTouchPoints(x, y); switch (motionEvent.getAction()) { case MotionEvent.ACTION_DOWN: { - if (pointerCount == 1) - mIsTap = true; - - // display lat-lon under first finger down - +// eventSource.getSceneController().setPickPoint(touchPoint); + mInitialX = x; + mInitialY = y; + this.setSelectedPosition(this.computeSelectedPosition()); break; } // all fingers have left the tablet screen case MotionEvent.ACTION_UP: { - if (mIsTap && pointerCount == 1) - { - long curTime = System.currentTimeMillis(); - long timeSinceLastTap = curTime - mLastTap; - - // double tap has occurred - if (mLastTap > 0 && (timeSinceLastTap < DOUBLE_TAP_INTERVAL)) - { - // handle double tap here - mLastTap = 0; - } - // otherwise, single tap has occurred - else if (mLastTap < 0 || timeSinceLastTap > SINGLE_TAP_INTERVAL) - { - // handle single tap here - mLastTap = curTime; // last tap is now this tap - } - - eventSource.invokeInRenderingThread(new Runnable() - { - public void run() - { - displayLatLonAtScreenPoint(view.getContext(), x, y); - } - }); - eventSource.redraw(); - } - +// eventSource.getSceneController().setPickPoint(null); + this.setSelectedPosition(null); // reset previous variables mPreviousX = -1; mPreviousY = -1; mPreviousX2 = -1; mPreviousY2 = -1; - mPrevPinchWidth = -1; mPrevPointerCount = 0; break; } case MotionEvent.ACTION_MOVE: - float dx = 0; float dy = 0; if (mPreviousX > -1 && mPreviousY > -1) { dx = x - mPreviousX; dy = y - mPreviousY; - mIsTap = false; } // return if detect a new gesture, as indicated by a large jump if (Math.abs(dx) > JUMP_THRESHOLD || Math.abs(dy) > JUMP_THRESHOLD) @@ -147,16 +231,21 @@ public void run() // reset pinch variables mPreviousX2 = -1; mPreviousY2 = -1; - mPrevPinchWidth = -1; } // interpret the motionEvent - if (pointerCount == 1 && !mIsTap) + if (pointerCount == 1) { +// eventSource.getSceneController().setPickPoint(touchPoint); + final float forwardInput = y-mPreviousY; + final float sideInput = -(x-mPreviousX); + final float totalForwardInput = y-mInitialY; + final float totalSideInput = -(x-mInitialX); eventSource.invokeInRenderingThread(new Runnable() { public void run() { +// onHorizontalTranslateRel(forwardInput, sideInput, totalForwardInput, totalSideInput); handlePan(xVelocity, yVelocity); } }); @@ -184,25 +273,13 @@ else if (pointerCount > 1) } final double yVelocity2 = dy2 / height; - double pinchWidth = Math.sqrt(Math.pow((x - x2), 2) + Math.pow((y - y2), 2)); // compute angle traversed - final double deltaPinchWidth = pinchWidth - mPrevPinchWidth; final double deltaPinchAngle = computeRotationAngle(x, y, x2, y2, mPreviousX, mPreviousY, mPreviousX2, mPreviousY2); - if (mPrevPinchWidth > 0 && Math.abs(deltaPinchWidth) > PINCH_WIDTH_DELTA_THRESHOLD) - { - eventSource.invokeInRenderingThread(new Runnable() - { - public void run() - { - handlePinchZoom(deltaPinchWidth); - } - }); - } // TODO: prevent this from confusion with pinch-rotate - else if ((upMove || downMove) && Math.abs(slope) > 1 + if ((upMove || downMove) && Math.abs(slope) > 1 && (yVelocity > 0 && yVelocity2 > 0) || (yVelocity < 0 && yVelocity2 < 0)) { eventSource.invokeInRenderingThread(new Runnable() @@ -226,7 +303,6 @@ public void run() mPreviousX2 = x2; mPreviousY2 = y2; - mPrevPinchWidth = pinchWidth; } else if (pointerCount >= 3) { @@ -245,58 +321,18 @@ public void run() mPreviousX = x; mPreviousY = y; mPrevPointerCount = pointerCount; - break; } return true; } - protected void displayLatLonAtScreenPoint(Context context, float x, float y) - { - BasicView view = (BasicView) this.eventSource.getView(); - Globe globe = this.eventSource.getModel().getGlobe(); - this.screenPoint.set((int) x, (int) y); - - if (view.computePositionFromScreenPoint(globe, this.screenPoint, this.position)) - { - final String latText = this.position.latitude.toString(); - final String lonText = this.position.longitude.toString(); - - ((Activity) context).runOnUiThread(new Runnable() - { - public void run() - { - updateLatLonText(latText, lonText); - } - }); - } - else - { - ((Activity) context).runOnUiThread(new Runnable() - { - public void run() - { - updateLatLonText("off globe", "off globe"); - } - }); - } - } - - protected void updateLatLonText(String latitudeText, String longitudeText) - { - // update displayed lat/lon - TextView latText = ((WorldWindowGLSurfaceView) this.eventSource).getLatitudeText(); - TextView lonText = ((WorldWindowGLSurfaceView) this.eventSource).getLongitudeText(); + protected void updateTouchPoints(float x, float y) { + previousTouchPoint.set(touchPoint.x, touchPoint.y); + touchPoint.set((int)x, (int)y); + } - if (latText != null && lonText != null) - { - latText.setText(latitudeText); - lonText.setText(longitudeText); - } - } - - // given the current and previous locations of two points, compute the angle of the + // given the current and previous locations of two points, compute the angle of the // rotation they trace out protected double computeRotationAngle(float x, float y, float x2, float y2, float xPrev, float yPrev, float xPrev2, float yPrev2) @@ -368,8 +404,10 @@ protected double computeRotationAngle(float x, float y, float x2, float y2, // computes pan using velocity of swipe motion protected void handlePan(double xVelocity, double yVelocity) { - BasicView view = (BasicView) this.eventSource.getView(); - Position pos = view.getLookAtPosition(); + stopAnimations(); + BasicView view = (BasicView) eventSource.getView(); + + Position pos = view.getLookAtPosition(); Angle heading = view.getHeading(); double range = view.getRange(); @@ -385,22 +423,31 @@ protected void handlePan(double xVelocity, double yVelocity) pos.setDegrees(newLat, newLon); } - protected void handlePinchZoom(double widthDelta) + private void stopAnimations() { + animationHandler.sendEmptyMessage(WHAT_STOP_ANIMATIONS); +// ((View)eventSource).post(new Runnable() { +// @Override +// public void run() { +// ((BasicView) eventSource.getView()).stopAnimations(); +// } +// }); + } + + protected Position getSelectedPosition() + { + return this.selectedPosition; + } + + protected void setSelectedPosition(Position position) + { + this.selectedPosition = position; + } + + protected void handlePinchRotate(double rotAngleDegrees) { - BasicView view = (BasicView) this.eventSource.getView(); - double value = view.getRange(); - double zoomScalingFactor = 3E-3f; - double newValue = value - widthDelta * zoomScalingFactor * value; - - if (newValue < 0) - newValue = 0; - - view.setRange(newValue); - } + stopAnimations(); - protected void handlePinchRotate(double rotAngleDegrees) - { - BasicView view = (BasicView) this.eventSource.getView(); + BasicView view = (BasicView) this.eventSource.getView(); Angle angle = view.getHeading(); double newAngle = (angle.degrees - rotAngleDegrees) % 360; @@ -414,7 +461,9 @@ else if (newAngle > 180) protected void handleLookAtTilt(double yVelocity) { - BasicView view = (BasicView) this.eventSource.getView(); + stopAnimations(); + + BasicView view = (BasicView) this.eventSource.getView(); Angle angle = view.getTilt(); double scalingFactor = 100; double newAngle = (angle.degrees + yVelocity * scalingFactor) % 360; @@ -443,4 +492,256 @@ protected void handleRestoreNorth(double xVelocity, double yVelocity) heading.setDegrees(newHeading); tilt.setDegrees(newTilt); } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + BasicView view = (BasicView) this.eventSource.getView(); + double range = view.getRange(); + double newRange = range/detector.getScaleFactor(); + view.setRange(newRange); + return true; + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + stopAnimations(); + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + } + + protected Position computeSelectedPosition() { + BasicView view = (BasicView) this.eventSource.getView(); + if (view == null) // include this test to ensure any derived implementation performs it + { + return null; + } + selectedPosition = new Position(); + view.computePositionFromScreenPoint(eventSource.getModel().getGlobe(), touchPoint, selectedPosition); + return selectedPosition; + } + +/* + protected Position computeSelectedPosition() + { + PickedObjectList pickedObjects = this.eventSource.getObjectsAtCurrentPosition(); + if (pickedObjects != null) + { + PickedObject top = pickedObjects.getTopPickedObject(); + if (top != null && top.isTerrain()) + { + return top.getPosition(); + } + } + return null; + } +*/ + protected Vec4 computeSelectedPointAt(Point point) + { + if (this.getSelectedPosition() == null) + { + return null; + } + + BasicView view = (BasicView) this.eventSource.getView(); + if (view == null) + { + return null; + } + // Reject a selected position if its elevation is above the eye elevation. When that happens, the user is + // essentially dragging along the inside of a sphere, and the effects of dragging are reversed. To the user + // this behavior appears unpredictable. + double elevation = this.getSelectedPosition().elevation; + if (view.getEyePosition().elevation <= elevation) + { + return null; + } + + // Intersect with a somewhat larger or smaller Globe which will pass through the selected point, but has the + // same proportions as the actual Globe. This will simulate dragging the selected position more accurately. + Line ray = new Line(); + view.computeRayFromScreenPoint(point, ray); + Intersection[] intersections = this.eventSource.getModel().getGlobe().intersect(ray); + if (intersections == null || intersections.length == 0) + { + return null; + } + + return ray.nearestIntersectionPoint(intersections); + } + + protected LatLon getChangeInLocation(Point point1, Point point2, Vec4 vec1, Vec4 vec2) + { + // Modify the distance we'll actually travel based on the slope of world distance travelled to screen + // distance travelled . A large slope means the user made a small change in screen space which resulted + // in a large change in world space. We want to reduce the impact of that change to something reasonable. + + double dragSlope = this.computeDragSlope(point1, point2, vec1, vec2); + double dragSlopeFactor = this.getDragSlopeFactor(); + double scale = 1.0 / (1.0 + dragSlopeFactor * dragSlope * dragSlope); + + Position pos1 = this.eventSource.getModel().getGlobe().computePositionFromPoint(vec1); + Position pos2 = this.eventSource.getModel().getGlobe().computePositionFromPoint(vec2); + LatLon adjustedLocation = LatLon.interpolateGreatCircle(scale, pos1, pos2); + + // Return the distance to travel in angular degrees. + return pos1.subtract(adjustedLocation); + } + + public double computeDragSlope(Point point1, Point point2, Vec4 vec1, Vec4 vec2) + { + BasicView view = (BasicView) eventSource.getView(); + if (view == null) + { + return 0.0; + } + + // Compute the screen space distance between point1 and point2. + double dx = point2.x - point1.x; + double dy = point2.y - point1.y; + double pixelDistance = Math.sqrt(dx * dx + dy * dy); + + // Determine the distance from the eye to the point on the forward vector closest to vec1 and vec2 + double d = view.getEyePoint().distanceTo3(vec1); + // Compute the size of a screen pixel at the nearest of the two distances. + double pixelSize = view.computePixelSizeAtDistance(d); + + // Return the ratio of world distance to screen distance. + double slope = vec1.distanceTo3(vec2) / (pixelDistance * pixelSize); + if (slope < 1.0) + slope = 1.0; + + return slope - 1.0; + } + + protected void onHorizontalTranslateAbs(Angle latitudeChange, Angle longitudeChange) + { + stopAnimations(); + + BasicView view = (BasicView) this.eventSource.getView(); + if (view == null) // include this test to ensure any derived implementation performs it + { + return; + } + + if (latitudeChange.equals(Angle.ZERO) && longitudeChange.equals(Angle.ZERO)) + { + return; + } + + Position newPosition = view.getLookAtPosition().add(new Position( + latitudeChange, longitudeChange, 0.0)); + + view.setLookAtPosition(newPosition); + } + + protected void onHorizontalTranslateRel(double forwardInput, double sideInput, double totalForwardInput, double totalSideInput) + { + stopAnimations(); + + // Normalize the forward and right magnitudes. + double length = Math.sqrt(forwardInput * forwardInput + sideInput * sideInput); + if (length > 0.0) + { + forwardInput /= length; + sideInput /= length; + } + + Point point = touchPoint; + Point lastPoint = previousTouchPoint; + if (getSelectedPosition() == null) + { + // Compute the current selected position if none exists. This happens if the user starts dragging when + // the cursor is off the globe, then drags the cursor onto the globe. + setSelectedPosition(computeSelectedPosition()); + } + else if (computeSelectedPosition() == null) + { + // User dragged the cursor off the globe. Clear the selected position to ensure a new one will be + // computed if the user drags the cursor back to the globe. + setSelectedPosition(null); + } + else if (computeSelectedPointAt(point) == null || computeSelectedPointAt(lastPoint) == null) + { + // User selected a position that is won't work for dragging. Probably the selected elevation is above the + // eye elevation, in which case dragging becomes unpredictable. Clear the selected position to ensure + // a new one will be computed if the user drags the cursor to a valid position. + setSelectedPosition(null); + } + + Vec4 vec = computeSelectedPointAt(point); + Vec4 lastVec = computeSelectedPointAt(lastPoint); + + // Cursor is on the globe, pan between the two positions. + if (vec != null && lastVec != null) + { + // Compute the change in view location given two screen points and corresponding world vectors. + LatLon latlon = getChangeInLocation(lastPoint, point, lastVec, vec); + onHorizontalTranslateAbs(latlon.getLatitude(), latlon.getLongitude()); + return; + } + + forwardInput = point.y - lastPoint.y; + sideInput = -(point.x-lastPoint.x); + + // Cursor is off the globe, we potentially want to simulate globe dragging. + // or this is a keyboard event. + Angle forwardChange = Angle.fromDegrees( + forwardInput * getScaleValueHorizTransRel()); + Angle sideChange = Angle.fromDegrees( + sideInput * getScaleValueHorizTransRel()); + onHorizontalTranslateRel(forwardChange, sideChange); + } + + protected void onHorizontalTranslateRel(Angle forwardChange, Angle sideChange) + { + BasicView view = (BasicView) this.eventSource.getView(); + if (view == null) // include this test to ensure any derived implementation performs it + { + return; + } + + if (forwardChange.equals(Angle.ZERO) && sideChange.equals(Angle.ZERO)) + { + return; + } + + double sinHeading = view.getHeading().sin(); + double cosHeading = view.getHeading().cos(); + double latChange = cosHeading * forwardChange.getDegrees() - sinHeading * sideChange.getDegrees(); + double lonChange = sinHeading * forwardChange.getDegrees() + cosHeading * sideChange.getDegrees(); + Position newPosition = view.getLookAtPosition().add(Position.fromDegrees(latChange, lonChange, 0.0)); + + view.setLookAtPosition(newPosition); + } + + protected double getScaleValueHorizTransRel() + { + BasicView view = (BasicView) this.eventSource.getView(); + if (view == null) + { + return 0.0; + } + double[] range = { 0.00001, 0.2}; + // If this is an OrbitView, we use the zoom value to set the scale + double radius = this.eventSource.getModel().getGlobe().getRadius(); + double t = getScaleValue(range[0], range[1], + view.getRange(), 3.0 * radius, true); + return (t); + } + + protected double getScaleValue(double minValue, double maxValue, + double value, double range, boolean isExp) + { + double t = value / range; + t = t < 0 ? 0 : (t > 1 ? 1 : t); + if (isExp) + { + t = Math.pow(2.0, t) - 1.0; + } + return(minValue * (1.0 - t) + maxValue * t); + } + } \ No newline at end of file diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/event/InputHandler.java b/WorldWindAndroid/src/gov/nasa/worldwind/event/InputHandler.java index 51af983..7a36c65 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/event/InputHandler.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/event/InputHandler.java @@ -17,4 +17,8 @@ public interface InputHandler extends WWObject, View.OnTouchListener WorldWindow getEventSource(); void setEventSource(WorldWindow eventSource); + + void addSelectListener(SelectListener listener); + + void removeSelectListener(SelectListener listener); } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/event/NoOpInputHandler.java b/WorldWindAndroid/src/gov/nasa/worldwind/event/NoOpInputHandler.java index e9754cb..9362d70 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/event/NoOpInputHandler.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/event/NoOpInputHandler.java @@ -28,6 +28,16 @@ public void setEventSource(WorldWindow eventSource) { } + @Override + public void addSelectListener(SelectListener listener) { + + } + + @Override + public void removeSelectListener(SelectListener listener) { + + } + public boolean onTouch(View view, MotionEvent motionEvent) { return false; diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/event/PositionEvent.java b/WorldWindAndroid/src/gov/nasa/worldwind/event/PositionEvent.java new file mode 100644 index 0000000..3a9ac71 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/event/PositionEvent.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.event; + +import android.graphics.Point; +import gov.nasa.worldwind.geom.Position; + +/** + * @author tag + * @version $Id$ + */ +public class PositionEvent extends WWEvent +{ + private final Point screenPoint; + private final Position position; + private final Position previousPosition; + + public PositionEvent(Object source, Point screenPoint, Position previousPosition, Position position) + { + super(source); + this.screenPoint = screenPoint; + this.position = position; + this.previousPosition = previousPosition; + } + + public Point getScreenPoint() + { + return screenPoint; + } + + public Position getPosition() + { + return position; + } + + public Position getPreviousPosition() + { + return previousPosition; + } + + @Override + public String toString() + { + return this.getClass().getName() + " " + + (this.previousPosition != null ? this.previousPosition : "null") + + " --> " + + (this.position != null ? this.position : "null"); + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/event/PositionListener.java b/WorldWindAndroid/src/gov/nasa/worldwind/event/PositionListener.java new file mode 100644 index 0000000..c126015 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/event/PositionListener.java @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2011 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.event; + +import java.util.EventListener; + +/** + * @author tag + * @version $Id$ + */ +public interface PositionListener extends EventListener +{ + public void moved(PositionEvent event); +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/event/SelectEvent.java b/WorldWindAndroid/src/gov/nasa/worldwind/event/SelectEvent.java new file mode 100644 index 0000000..c52e1e6 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/event/SelectEvent.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.event; + +import android.graphics.Point; +import android.graphics.Rect; +import gov.nasa.worldwind.pick.PickedObject; +import gov.nasa.worldwind.pick.PickedObjectList; +import gov.nasa.worldwind.util.Logging; + +import java.awt.event.MouseEvent; +import java.util.List; + +/** + * This class signals that an object or terrain is under the cursor and identifies that object and the operation that + * caused the signal. See the Field Summary for a description of the possible operations. When a SelectEvent + * occurs, all select event listeners registered with the associated {@link gov.nasa.worldwind.WorldWindow} are called. + * Select event listeners are registered by calling {@link gov.nasa.worldwind.WorldWindow#addSelectListener(SelectListener)}. + *

+ * A ROLLOVER SelectEvent is generated every frame when the cursor is over a visible object either because + * the user moved it there or because the World Window was repainted and a visible object was found to be under the + * cursor. A ROLLOVER SelectEvent is also generated when there are no longer any objects under the cursor. + * Select events generated for objects under the cursor have a non-null pickPoint, and contain the top-most visible + * object of all objects at the cursor position. + *

+ * A BOX_ROLLOVER SelectEvent is generated every frame when the selection box intersects a visible object + * either because the user moved or expanded it or because the World Window was repainted and a visible object was found + * to intersect the box. A BOX_ROLLOVER SelectEvent is also generated when there are no longer any objects + * intersecting the selection box. Select events generated for objects intersecting the selection box have a non-null + * pickRectangle, and contain all top-most visible objects of all objects intersecting the selection box. + *

+ * If a select listener performs some action in response to a select event, it should call the event's {@link + * #consume()} method in order to indicate to subsequently called listeners that the event has been responded to and no + * further action should be taken. + *

+ * If no object is under the cursor but the cursor is over terrain, the select event will identify the terrain as the + * picked object and will include the corresponding geographic position. See {@link + * gov.nasa.worldwind.pick.PickedObject#isTerrain()}. + * + * @author tag + * @version $Id$ + */ +@SuppressWarnings( {"StringEquality"}) +public class SelectEvent extends WWEvent +{ + /** The user clicked the left mouse button while the cursor was over picked object. */ + public static final String LEFT_CLICK = "gov.nasa.worldwind.SelectEvent.LeftClick"; + /** The user double-clicked the left mouse button while the cursor was over picked object. */ + public static final String LEFT_DOUBLE_CLICK = "gov.nasa.worldwind.SelectEvent.LeftDoubleClick"; + /** The user clicked the right mouse button while the cursor was over picked object. */ + public static final String RIGHT_CLICK = "gov.nasa.worldwind.SelectEvent.RightClick"; + /** The user pressed the left mouse button while the cursor was over picked object. */ + public static final String LEFT_PRESS = "gov.nasa.worldwind.SelectEvent.LeftPress"; + /** The user pressed the right mouse button while the cursor was over picked object. */ + public static final String RIGHT_PRESS = "gov.nasa.worldwind.SelectEvent.RightPress"; + /** + * The cursor has moved over the picked object and become stationary, or has moved off the object of the most recent + * HOVER event. In the latter case, the picked object will be null. + */ + public static final String HOVER = "gov.nasa.worldwind.SelectEvent.Hover"; + /** + * The cursor has moved over the object or has moved off the object most recently rolled over. In the latter case + * the picked object will be null. + */ + public static final String ROLLOVER = "gov.nasa.worldwind.SelectEvent.Rollover"; + /** The user is attempting to drag the picked object. */ + public static final String DRAG = "gov.nasa.worldwind.SelectEvent.Drag"; + /** The user has stopped dragging the picked object. */ + public static final String DRAG_END = "gov.nasa.worldwind.SelectEvent.DragEnd"; + /** + * The user has selected one or more of objects using a selection box. A box rollover event is generated every frame + * if one or more objects intersect the box, in which case the event's pickedObjects list contain the selected + * objects. A box rollover event is generated once when the selection becomes empty, in which case the event's + * pickedObjects is null. In either case, the event's pickRect contains the selection box bounds in AWT + * screen coordinates. + */ + public static final String BOX_ROLLOVER = "gov.nasa.worldwind.SelectEvent.BoxRollover"; + + private final String eventAction; + private final Point pickPoint; + private final Rect pickRect; + private final PickedObjectList pickedObjects; + + public SelectEvent(Object source, String eventAction, Point pickPoint, PickedObjectList pickedObjects) + { + super(source); + this.eventAction = eventAction; + this.pickPoint = pickPoint; + this.pickRect = null; + this.pickedObjects = pickedObjects; + } + + public SelectEvent(Object source, String eventAction, Rect pickRectangle, PickedObjectList pickedObjects) + { + super(source); + this.eventAction = eventAction; + this.pickPoint = null; + this.pickRect = pickRectangle; + this.pickedObjects = pickedObjects; + } + + @Override + public void consume() + { + super.consume(); + } + + public String getEventAction() + { + return this.eventAction != null ? this.eventAction : "gov.nasa.worldwind.SelectEvent.UnknownEventAction"; + } + + public Point getPickPoint() + { + return this.pickPoint; + } + + public Rect getPickRectangle() + { + return this.pickRect; + } + + + public boolean hasObjects() + { + return this.pickedObjects != null && this.pickedObjects.size() > 0; + } + + public PickedObjectList getObjects() + { + return this.pickedObjects; + } + + public PickedObject getTopPickedObject() + { + return this.hasObjects() ? this.pickedObjects.getTopPickedObject() : null; + } + + public Object getTopObject() + { + PickedObject tpo = this.getTopPickedObject(); + return tpo != null ? tpo.getObject() : null; + } + + /** + * Returns a list of all picked objects in this event's picked object list who's onTop flag is set to true. This + * returns null if this event's picked object list is empty, or does not contain any picked objects + * marked as on top. + * + * @return a new list of the picked objects marked as on top, or null if nothing is marked as on top. + */ +// public List getAllTopPickedObjects() +// { +// return this.hasObjects() ? this.pickedObjects.getAllTopPickedObjects() : null; +// } + + /** + * Returns a list of all objects associated with a picked object in this event's picked object list who's onTop flag + * is set to true. This returns null if this event's picked object list is empty, or does not contain + * any picked objects marked as on top. + * + * @return a new list of the objects associated with a picked object marked as on top, or null if + * nothing is marked as on top. + */ +// public List getAllTopObjects() +// { +// return this.hasObjects() ? this.pickedObjects. getAllTopObjects() : null; +// } + + public boolean isRollover() + { + return this.getEventAction() == ROLLOVER; + } + + public boolean isHover() + { + return this.getEventAction() == HOVER; + } + + public boolean isDragEnd() + { + return this.getEventAction() == DRAG_END; + } + + public boolean isDrag() + { + return this.getEventAction() == DRAG; + } + + public boolean isRightPress() + { + return this.getEventAction() == RIGHT_PRESS; + } + + public boolean isRightClick() + { + return this.getEventAction() == RIGHT_CLICK; + } + + public boolean isLeftDoubleClick() + { + return this.getEventAction() == LEFT_DOUBLE_CLICK; + } + + public boolean isLeftClick() + { + return this.getEventAction() == LEFT_CLICK; + } + + public boolean isLeftPress() + { + return this.getEventAction() == LEFT_PRESS; + } + + public boolean isBoxSelect() + { + return this.getEventAction() == BOX_ROLLOVER; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(this.getClass().getName() + " " + + (this.eventAction != null ? this.eventAction : Logging.getMessage("generic.Unknown"))); + if (this.pickedObjects != null && this.pickedObjects.getTopObject() != null) + sb.append(", ").append(this.pickedObjects.getTopObject().getClass().getName()); + + return sb.toString(); + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/event/SelectListener.java b/WorldWindAndroid/src/gov/nasa/worldwind/event/SelectListener.java new file mode 100644 index 0000000..7d68cdb --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/event/SelectListener.java @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2011 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.event; + +import java.util.EventListener; + +/** + * @author tag + * @version $Id$ + */ +public interface SelectListener extends EventListener +{ + public void selected(SelectEvent event); +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/geom/Angle.java b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Angle.java index 5678249..71721ec 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/geom/Angle.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Angle.java @@ -5,14 +5,48 @@ */ package gov.nasa.worldwind.geom; +import android.os.Parcel; +import android.os.Parcelable; import gov.nasa.worldwind.util.Logging; /** * @author dcollins * @version $Id: Angle.java 733 2012-09-02 17:15:09Z dcollins $ */ -public class Angle +public class Angle implements Comparable, Parcelable { + // Angle format + public final static String ANGLE_FORMAT_DD = "gov.nasa.worldwind.Geom.AngleDD"; + public final static String ANGLE_FORMAT_DMS = "gov.nasa.worldwind.Geom.AngleDMS"; + + /** Represents an angle of zero degrees */ + public final static Angle ZERO = Angle.fromDegrees(0); + + /** Represents a right angle of positive 90 degrees */ + public final static Angle POS90 = Angle.fromDegrees(90); + + /** Represents a right angle of negative 90 degrees */ + public final static Angle NEG90 = Angle.fromDegrees(-90); + + /** Represents an angle of positive 180 degrees */ + public final static Angle POS180 = Angle.fromDegrees(180); + + /** Represents an angle of negative 180 degrees */ + public final static Angle NEG180 = Angle.fromDegrees(-180); + + /** Represents an angle of positive 360 degrees */ + public final static Angle POS360 = Angle.fromDegrees(360); + + /** Represents an angle of negative 360 degrees */ + public final static Angle NEG360 = Angle.fromDegrees(-360); + + /** Represents an angle of 1 minute */ + public final static Angle MINUTE = Angle.fromDegrees(1d / 60d); + + /** Represents an angle of 1 second */ + public final static Angle SECOND = Angle.fromDegrees(1d / 3600d); + + protected static final double DEGREES_TO_RADIANS = Math.PI / 180d; protected static final double RADIANS_TO_DEGREES = 180d / Math.PI; @@ -29,6 +63,14 @@ protected Angle(double degrees, double radians) this.radians = radians; } + public double getDegrees() { + return degrees; + } + + public double getRadians() { + return radians; + } + /** * Obtains an angle from a specified number of degrees. * @@ -41,6 +83,44 @@ public static Angle fromDegrees(double degrees) return new Angle(degrees, DEGREES_TO_RADIANS * degrees); } + private static final double PIOver2 = Math.PI / 2; + + public static Angle fromDegreesLatitude(double degrees) + { + degrees = degrees < -90 ? -90 : degrees > 90 ? 90 : degrees; + double radians = DEGREES_TO_RADIANS * degrees; + radians = radians < -PIOver2 ? -PIOver2 : radians > PIOver2 ? PIOver2 : radians; + + return new Angle(degrees, radians); + } + + public static Angle fromRadiansLatitude(double radians) + { + radians = radians < -PIOver2 ? -PIOver2 : radians > PIOver2 ? PIOver2 : radians; + double degrees = RADIANS_TO_DEGREES * radians; + degrees = degrees < -90 ? -90 : degrees > 90 ? 90 : degrees; + + return new Angle(degrees, radians); + } + + public static Angle fromDegreesLongitude(double degrees) + { + degrees = degrees < -180 ? -180 : degrees > 180 ? 180 : degrees; + double radians = DEGREES_TO_RADIANS * degrees; + radians = radians < -Math.PI ? -Math.PI : radians > Math.PI ? Math.PI : radians; + + return new Angle(degrees, radians); + } + + public static Angle fromRadiansLongitude(double radians) + { + radians = radians < -Math.PI ? -Math.PI : radians > Math.PI ? Math.PI : radians; + double degrees = RADIANS_TO_DEGREES * radians; + degrees = degrees < -180 ? -180 : degrees > 180 ? 180 : degrees; + + return new Angle(degrees, radians); + } + /** * Obtains an angle from a specified number of radians. * @@ -85,22 +165,34 @@ public Angle set(Angle angle) return this; } - public Angle setDegrees(double degrees) + public void setDegrees(double degrees) { this.degrees = degrees; this.radians = DEGREES_TO_RADIANS * degrees; - - return this; } - public Angle setRadians(double radians) + public void setRadians(double radians) { this.degrees = RADIANS_TO_DEGREES * radians; this.radians = radians; - - return this; } + public void setDegreesF(float degrees) { + setDegrees(degrees); + } + + public void setRadiansF(float radians) { + setRadians(radians); + } + + public float getDegreesF() { + return (float)degrees; + } + + public float getRadiansF() { + return (float)radians; + } + /** * Obtains the sine of this angle. * @@ -208,7 +300,8 @@ public Angle addAndSet(Angle angle) throw new IllegalArgumentException(msg); } - return this.setDegrees(this.degrees + angle.degrees); + this.setDegrees(this.degrees + angle.degrees); + return this; } public Angle addAndSet(Angle lhs, Angle rhs) @@ -227,17 +320,20 @@ public Angle addAndSet(Angle lhs, Angle rhs) throw new IllegalArgumentException(msg); } - return this.setDegrees(lhs.degrees + rhs.degrees); + this.setDegrees(lhs.degrees + rhs.degrees); + return this; } public Angle addDegreesAndSet(double degrees) { - return this.setDegrees(this.degrees + degrees); + this.setDegrees(this.degrees + degrees); + return this; } public Angle addRadiansAndSet(double radians) { - return this.setRadians(this.radians + radians); + this.setRadians(this.radians + radians); + return this; } public Angle subtract(Angle angle) @@ -280,7 +376,8 @@ public Angle subtractAndSet(Angle angle) throw new IllegalArgumentException(msg); } - return this.setDegrees(this.degrees - angle.degrees); + this.setDegrees(this.degrees - angle.degrees); + return this; } public Angle subtractAndSet(Angle lhs, Angle rhs) @@ -299,17 +396,20 @@ public Angle subtractAndSet(Angle lhs, Angle rhs) throw new IllegalArgumentException(msg); } - return this.setDegrees(lhs.degrees - rhs.degrees); + this.setDegrees(lhs.degrees - rhs.degrees); + return this; } public Angle subtractDegreesAndSet(double degrees) { - return this.setDegrees(this.degrees - degrees); + this.setDegrees(this.degrees - degrees); + return this; } public Angle subtractRadiansAndSet(double radians) { - return this.setRadians(this.radians - radians); + this.setRadians(this.radians - radians); + return this; } /** @@ -327,7 +427,8 @@ public Angle multiply(double value) public Angle multiplyAndSet(double value) { - return this.setDegrees(this.degrees * value); + this.setDegrees(this.degrees * value); + return this; } public Angle multiplyAndSet(Angle angle, double value) @@ -339,7 +440,8 @@ public Angle multiplyAndSet(Angle angle, double value) throw new IllegalArgumentException(msg); } - return this.setDegrees(angle.degrees * value); + this.setDegrees(angle.degrees * value); + return this; } /** @@ -390,6 +492,34 @@ public int hashCode() return (int) (temp ^ (temp >>> 32)); } + /** + * Compares this {@link Angle} with another. Returns a negative integer if this is the smaller angle, a positive + * integer if this is the larger, and zero if both angles are equal. + * + * @param angle the angle to compare against. + * + * @return -1 if this angle is smaller, 0 if both are equal and +1 if this angle is larger. + * + * @throws IllegalArgumentException if angle is null. + */ + public final int compareTo(Angle angle) + { + if (angle == null) + { + String msg = Logging.getMessage("nullValue.AngleIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + if (this.degrees < angle.degrees) + return -1; + + if (this.degrees > angle.degrees) + return 1; + + return 0; + } + /** * Obtains the amount of memory this {@link Angle} consumes. * @@ -400,6 +530,37 @@ public long getSizeInBytes() return Double.SIZE / 8; } + /** + * Obtains a {@link String} representation of this {@link Angle} formated as degrees, minutes and seconds + * integer values. + * + * @return the value of this angle in degrees, minutes, seconds as a string. + */ + public final String toDMSString() + { + double temp = this.degrees; + int sign = (int) Math.signum(temp); + temp *= sign; + int d = (int) Math.floor(temp); + temp = (temp - d) * 60d; + int m = (int) Math.floor(temp); + temp = (temp - m) * 60d; + int s = (int) Math.round(temp); + + if (s == 60) + { + m++; + s = 0; + } // Fix rounding errors + if (m == 60) + { + d++; + m = 0; + } + + return (sign == -1 ? "-" : "") + d + '\u00B0' + ' ' + m + '\u2019' + ' ' + s + '\u201d'; + } + /** * Obtains a String representation of this angle. * @@ -410,4 +571,95 @@ public String toString() { return Double.toString(this.degrees) + '\u00B0'; } + + public static Angle normalizedLatitude(Angle unnormalizedAngle) + { + if (unnormalizedAngle == null) + { + String msg = Logging.getMessage("nullValue.AngleIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + return Angle.fromDegrees(normalizedDegreesLatitude(unnormalizedAngle.degrees)); + } + + public static Angle normalizedLongitude(Angle unnormalizedAngle) + { + if (unnormalizedAngle == null) + { + String msg = Logging.getMessage("nullValue.AngleIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + return Angle.fromDegrees(normalizedDegreesLongitude(unnormalizedAngle.degrees)); + } + + public Angle normalizedLatitude() + { + return normalizedLatitude(this); + } + + public Angle normalizedLongitude() + { + return normalizedLongitude(this); + } + + /** + * Linearly interpolates between two angles. + * + * @param amount the interpolant. + * @param value1 the first angle. + * @param value2 the second angle. + * + * @return a new angle between value1 and value2. + */ + public static Angle mix(double amount, Angle value1, Angle value2) + { + if (value1 == null || value2 == null) + { + String message = Logging.getMessage("nullValue.AngleIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (amount < 0) + return value1; + else if (amount > 1) + return value2; + + Quaternion quat = Quaternion.slerp( + amount, + Quaternion.fromAxisAngle(value1, Vec4.UNIT_X), + Quaternion.fromAxisAngle(value2, Vec4.UNIT_X)); + + Angle angle = quat.getRotationX(); + if (Double.isNaN(angle.degrees)) + return null; + + return angle; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeDouble(this.degrees); + } + + public static final Creator CREATOR = new Creator() { + @Override + public Angle createFromParcel(Parcel in) { + return Angle.fromDegrees(in.readDouble()); + } + + @Override + public Angle[] newArray(int size) { + return new Angle[size]; + } + }; } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/geom/Box.java b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Box.java index 8f7ec13..4276197 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/geom/Box.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Box.java @@ -6,9 +6,18 @@ package gov.nasa.worldwind.geom; +import android.opengl.GLES20; +import gov.nasa.worldwind.R; +import gov.nasa.worldwind.cache.GpuResourceCache; +import gov.nasa.worldwind.render.Color; +import gov.nasa.worldwind.render.DrawContext; +import gov.nasa.worldwind.render.GpuProgram; +import gov.nasa.worldwind.render.Renderable; import gov.nasa.worldwind.util.*; -import java.nio.FloatBuffer; +import java.nio.*; + +import static android.opengl.GLES20.*; /** * An arbitrarily oriented box, typically used as a oriented bounding volume for a collection of points or shapes. A @@ -19,7 +28,7 @@ * @author tag * @version $Id: Box.java 807 2012-09-26 17:40:25Z dcollins $ */ -public class Box implements Extent +public class Box implements Extent, Renderable { protected final Vec4 bottomCenter; // point at center of box's longest axis protected final Vec4 topCenter; // point at center of box's longest axis @@ -458,6 +467,32 @@ public double getTLength() return tLength; } + /** + * Returns the eight corners of the box. + * + * @return the eight box corners in the order bottom-lower-left, bottom-lower-right, bottom-upper-right, + * bottom-upper-left, top-lower-left, top-lower-right, top-upper-right, top-upper-left. + */ + public Vec4[] getCorners() + { + Vec4 ll = this.s.add3(this.t).multiply3(-0.5); // Lower left. + Vec4 lr = this.t.subtract3(this.s).multiply3(0.5); // Lower right. + Vec4 ur = this.s.add3(this.t).multiply3(0.5); // Upper right. + Vec4 ul = this.s.subtract3(this.t).multiply3(0.5); // Upper left. + + Vec4[] corners = new Vec4[8]; + corners[0] = this.bottomCenter.add3(ll); + corners[1] = this.bottomCenter.add3(lr); + corners[2] = this.bottomCenter.add3(ur); + corners[3] = this.bottomCenter.add3(ul); + corners[4] = this.topCenter.add3(ll); + corners[5] = this.topCenter.add3(lr); + corners[6] = this.topCenter.add3(ur); + corners[7] = this.topCenter.add3(ul); + + return corners; + } + public Box translate(Vec4 point) { if (point == null) @@ -517,6 +552,41 @@ public double getEffectiveRadius(Plane plane) return 0.5 * (Math.abs(this.s.dot3(n)) + Math.abs(this.t.dot3(n))); } + + + /** {@inheritDoc} */ + public boolean intersects(Plane plane) + { + if (plane == null) + { + String message = Logging.getMessage("nullValue.PlaneIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + double effectiveRadius = this.getEffectiveRadius(plane); + return this.intersects(plane, effectiveRadius) >= 0; + } + + protected double intersects(Plane plane, double effectiveRadius) + { + // Test the distance from the first end-point. + double dq1 = plane.dot(this.bottomCenter); + boolean bq1 = dq1 <= -effectiveRadius; + + // Test the distance from the top of the box. + double dq2 = plane.dot(this.topCenter); + boolean bq2 = dq2 <= -effectiveRadius; + + if (bq1 && bq2) // both beyond effective radius; box is on negative side of plane + return -1; + + if (bq1 == bq2) // both within effective radius; can't draw any conclusions + return 0; + + return 1; // box almost certainly intersects + } + /** {@inheritDoc} */ public boolean intersects(Frustum frustum) { @@ -595,6 +665,24 @@ protected double intersectsAt(Plane plane, double effectiveRadius, Vec4 endpoint return t; } + /** {@inheritDoc} */ + public Intersection[] intersect(Line line) + { + if (line == null) + { + String message = Logging.getMessage("nullValue.LineIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + return WWMath.polytopeIntersect(line, this.planes); + } + + /** {@inheritDoc} */ + public boolean intersects(Line line) + { + return intersect(line) != null; + } + @Override public boolean equals(Object o) { @@ -619,4 +707,168 @@ public int hashCode() result = 31 * result + (this.t != null ? this.t.hashCode() : 0); return result; } + + public String toString() + { + return String.format("Box @(%s) r: %s, s: %s, t: %s", center.toString(), r.toString(), s.toString(), t.toString()); + } + + private FloatBuffer vBuf = ByteBuffer.allocateDirect(6 * 3 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();; + private IntBuffer iBuf = ByteBuffer.allocateDirect(4 * 4).order(ByteOrder.nativeOrder()).asIntBuffer(); + protected final Object programKey = new Object(); + protected boolean programCreationFailed; + + /** + * Draws a representation of the Box. + * + * @param dc the DrawContext to be used. + */ + public void render(DrawContext dc) + { + if (dc == null) + { + String message = Logging.getMessage("nullValue.DocumentSourceIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (dc.isPickingMode()) + return; + + GpuProgram program = this.getGpuProgram(dc.getGpuResourceCache()); + if (program == null) return; + + dc.setCurrentProgram(program); + program.bind(); + + program.loadUniform1f("uOpacity", dc.isPickingMode() ? 1f : dc.getCurrentLayer().getOpacity()); + + int attribLocation = program.getAttribLocation("vertexPoint"); + if (attribLocation >= 0) GLES20.glEnableVertexAttribArray(attribLocation); + + Vec4 a = this.s.add3(this.t).multiply3(-0.5); + Vec4 b = this.s.subtract3(this.t).multiply3(0.5); + Vec4 c = this.s.add3(this.t).multiply3(0.5); + Vec4 d = this.t.subtract3(this.s).multiply3(0.5); + + OGLStackHandler ogsh = new OGLStackHandler(); + ogsh.pushAttrib(GL_COLOR_BUFFER_BIT // For alpha enable, blend enable, alpha func, blend func. + | GL_DEPTH_BUFFER_BIT); // For depth test enable, depth func. + try { + GLES20.glLineWidth(1f); + GLES20.glEnable(GLES20.GL_BLEND); + OGLUtil.applyBlending(false); + GLES20.glEnable(GLES20.GL_DEPTH_TEST); + + GLES20.glDepthFunc(GLES20.GL_LEQUAL); + dc.getCurrentProgram().loadUniformColor("uColor", new Color(1d, 1d, 1d, 0.5d)); + this.drawBox(dc, a, b, c, d); + + GLES20.glDepthFunc(GLES20.GL_GREATER); + dc.getCurrentProgram().loadUniformColor("uColor", new Color(1d, 0d, 1d, 0.4d)); + this.drawBox(dc, a, b, c, d); + + } finally { + ogsh.popAttrib(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + GLES20.glDisableVertexAttribArray(attribLocation); + + dc.setCurrentProgram(null); + GLES20.glUseProgram(0); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0); + } + } + + protected void drawBox(DrawContext dc, Vec4 a, Vec4 b, Vec4 c, Vec4 d) + { + Vec4 e = a.add3(this.r); + Vec4 f = d.add3(this.r); + vBuf.rewind(); + vBuf.put((float)a.x); + vBuf.put((float)a.y); + vBuf.put((float)a.z); + vBuf.put((float)b.x); + vBuf.put((float)b.y); + vBuf.put((float)b.z); + vBuf.put((float)c.x); + vBuf.put((float)c.y); + vBuf.put((float)c.z); + vBuf.put((float)d.x); + vBuf.put((float)d.y); + vBuf.put((float)d.z); + vBuf.put((float)e.x); + vBuf.put((float)e.y); + vBuf.put((float)e.z); + vBuf.put((float)f.x); + vBuf.put((float)f.y); + vBuf.put((float)f.z); + + Matrix matrix = Matrix.fromIdentity(); + matrix.multiplyAndSet(dc.getView().getModelviewProjectionMatrix(), +// Matrix.fromIdentity()); + Matrix.fromTranslation(this.bottomCenter)); + + dc.getCurrentProgram().loadUniformMatrix("mvpMatrix", matrix); + Matrix tmpMatrix = matrix.copy(); + + // Draw parallel lines in R direction + int n = 20; + Vec4 dr = this.r.multiply3(1d / (double) n); + + this.drawOutline(dc, 0, 1, 2, 3); + for (int i = 1; i < n; i++) + { + tmpMatrix.multiplyAndSet(Matrix.fromTranslation(dr)); + dc.getCurrentProgram().loadUniformMatrix("mvpMatrix", tmpMatrix); + this.drawOutline(dc, 0, 1, 2, 3); + } + + dc.getCurrentProgram().loadUniformMatrix("mvpMatrix", matrix); + tmpMatrix = matrix.copy(); + + // Draw parallel lines in S direction + n = 20; + Vec4 ds = this.s.multiply3(1d / (double) n); + + this.drawOutline(dc, 0, 4, 5, 3); + for (int i = 1; i < n; i++) + { + tmpMatrix.multiplyAndSet(Matrix.fromTranslation(ds)); + dc.getCurrentProgram().loadUniformMatrix("mvpMatrix", tmpMatrix); + this.drawOutline(dc, 0, 4, 5, 3); + } + } + + protected void drawOutline(DrawContext dc, int a, int b, int c, int d) + { + iBuf.rewind(); + iBuf.put(a); + iBuf.put(b); + iBuf.put(c); + iBuf.put(d); + int maPositionHandle = dc.getCurrentProgram().getAttribLocation("vertexPoint"); + GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false, 3, vBuf.rewind()); + GLES20.glDrawElements(GLES20.GL_LINE_LOOP, 4, GLES20.GL_UNSIGNED_INT, iBuf.rewind()); + } + + + protected GpuProgram getGpuProgram(GpuResourceCache cache) { + if (this.programCreationFailed) return null; + + GpuProgram program = cache.getProgram(this.programKey); + + if (program == null) { + try { + GpuProgram.GpuProgramSource source = GpuProgram.readProgramSource(R.raw.simple_vert, R.raw.uniform_color_frag); + program = new GpuProgram(source); + cache.put(this.programKey, program); + } catch (Exception e) { + String msg = Logging.getMessage("GL.ExceptionLoadingProgram", R.raw.simple_vert, R.raw.uniform_color_frag); + Logging.error(msg); + this.programCreationFailed = true; + } + } + + return program; + } } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/geom/Cylinder.java b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Cylinder.java index 21050d0..5f884f9 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/geom/Cylinder.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Cylinder.java @@ -225,6 +225,184 @@ protected double intersectsAt(Plane plane, double effectiveRadius, Vec4[] endpoi endpoints[1] = newEndPoint; return t; + } + + /** {@inheritDoc} */ + public Intersection[] intersect(Line line) + { + if (line == null) + { + String message = Logging.getMessage("nullValue.LineIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + double[] tVals = new double[2]; + if (!intcyl(line.getOrigin(), line.getDirection(), this.bottomCenter, this.axisUnitDirection, + this.cylinderRadius, tVals)) + return null; + + if (!clipcyl(line.getOrigin(), line.getDirection(), this.bottomCenter, this.topCenter, + this.axisUnitDirection, tVals)) + return null; + + Vec4 vec = new Vec4(); + Vec4 vec2 = new Vec4(); + + if (!Double.isInfinite(tVals[0]) && !Double.isInfinite(tVals[1]) && tVals[0] >= 0.0 && tVals[1] >= 0.0) { + return new Intersection[]{new Intersection(line.getPointAt(tVals[0], vec), false), + new Intersection(line.getPointAt(tVals[1], vec2), false)}; + } + if (!Double.isInfinite(tVals[0]) && tVals[0] >= 0.0) { + return new Intersection[]{new Intersection(line.getPointAt(tVals[0], vec), false)}; + } + if (!Double.isInfinite(tVals[1]) && tVals[1] >= 0.0) { + return new Intersection[]{new Intersection(line.getPointAt(tVals[1],vec), false)}; + } + return null; + } + + /** {@inheritDoc} */ + public boolean intersects(Line line) + { + return intersect(line) != null; + } + + // Taken from "Graphics Gems IV", Section V.2, page 356. + + protected boolean intcyl(Vec4 raybase, Vec4 raycos, Vec4 base, Vec4 axis, double radius, double[] tVals) + { + boolean hit; // True if ray intersects cyl + Vec4 RC; // Ray base to cylinder base + double d; // Shortest distance between the ray and the cylinder + double t, s; // Distances along the ray + Vec4 n, D, O; + double ln; + + RC = raybase.subtract3(base); + n = raycos.cross3(axis); + + // Ray is parallel to the cylinder's axis. + if ((ln = n.getLength3()) == 0.0) + { + d = RC.dot3(axis); + D = RC.subtract3(axis.multiply3(d)); + d = D.getLength3(); + tVals[0] = Double.NEGATIVE_INFINITY; + tVals[1] = Double.POSITIVE_INFINITY; + // True if ray is in cylinder. + return d <= radius; + } + + n = n.normalize3(); + d = Math.abs(RC.dot3(n)); // Shortest distance. + hit = (d <= radius); + + // If ray hits cylinder. + if (hit) + { + O = RC.cross3(axis); + t = -O.dot3(n) / ln; + O = n.cross3(axis); + O = O.normalize3(); + s = Math.abs(Math.sqrt(radius * radius - d * d) / raycos.dot3(O)); + tVals[0] = t - s; // Entering distance. + tVals[1] = t + s; // Exiting distance. + } + + return hit; + } + + // Taken from "Graphics Gems IV", Section V.2, page 356. + + protected boolean clipcyl(Vec4 raybase, Vec4 raycos, Vec4 bot, Vec4 top, Vec4 axis, double[] tVals) + { + double dc, dwb, dwt, tb, tt; + double in, out; // Object intersection distances. + + in = tVals[0]; + out = tVals[1]; + + dc = axis.dot3(raycos); + dwb = axis.dot3(raybase) - axis.dot3(bot); + dwt = axis.dot3(raybase) - axis.dot3(top); + + // Ray is parallel to the cylinder end-caps. + if (dc == 0.0) + { + if (dwb <= 0.0) + return false; + if (dwt >= 0.0) + return false; + } + else + { + // Intersect the ray with the bottom end-cap. + tb = -dwb / dc; + // Intersect the ray with the top end-cap. + tt = -dwt / dc; + + // Bottom is near cap, top is far cap. + if (dc >= 0.0) + { + if (tb > out) + return false; + if (tt < in) + return false; + if (tb > in && tb < out) + in = tb; + if (tt > in && tt < out) + out = tt; + } + // Bottom is far cap, top is near cap. + else + { + if (tb < in) + return false; + if (tt > out) + return false; + if (tb > in && tb < out) + out = tb; + if (tt > in && tt < out) + in = tt; + } + } + + tVals[0] = in; + tVals[1] = out; + return in < out; + } + + protected double intersects(Plane plane, double effectiveRadius) + { + if (plane == null) + { + String message = Logging.getMessage("nullValue.PlaneIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // Test the distance from the first cylinder end-point. Assumes that bottomCenter's w-coordinate is 1. + double dq1 = plane.dot(this.bottomCenter); + boolean bq1 = dq1 <= -effectiveRadius; + + // Test the distance from the top of the cylinder. Assumes that topCenter's w-coordinate is 1. + double dq2 = plane.dot(this.topCenter); + boolean bq2 = dq2 <= -effectiveRadius; + + if (bq1 && bq2) // both beyond effective radius; cylinder is on negative side of plane + return -1; + + if (bq1 == bq2) // both within effective radius; can't draw any conclusions + return 0; + + return 1; // Cylinder almost certainly intersects + } + + /** {@inheritDoc} */ + public boolean intersects(Plane plane) + { + return this.intersects(plane, getEffectiveRadius(plane)) >= 0; } @Override @@ -270,7 +448,7 @@ public int hashCode() public String toString() { - return this.cylinderRadius + ", " + this.bottomCenter.toString() + ", " + this.topCenter.toString() + ", " + return "Cylinder " + this.cylinderRadius + ", " + this.bottomCenter.toString() + ", " + this.topCenter.toString() + ", " + this.axisUnitDirection.toString(); } } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/geom/Extent.java b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Extent.java index fb66088..9448dc7 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/geom/Extent.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Extent.java @@ -29,18 +29,6 @@ public interface Extent */ double getRadius(); - /** - * Computes the distance between this extent and the specified point. This returns 0 if the point is inside this - * extent. This does not retain any reference to the specified point, or modify it in any way. - * - * @param point the point who's distance to this extent is computed. - * - * @return the distance between the point and this extent, or 0 if the point is inside this extent. - * - * @throws IllegalArgumentException if the point is null. - */ - double distanceTo(Vec4 point); - /** * Computes the effective radius of the extent relative to a specified plane. * @@ -59,4 +47,37 @@ public interface Extent * @return true if there is an intersection, false otherwise. */ boolean intersects(Frustum frustum); + + /** + * Computes the intersections of this extent with line. The returned array may be either null or of + * zero length if no intersections are discovered. It does not contain null elements. Tangential intersections are + * marked as such. line is considered to have infinite length in both directions. + * + * @param line the Line with which to intersect this Extent. + * + * @return an array of intersections representing all the points where line enters or leave this + * Extent. + */ + Intersection[] intersect(Line line); + + /** + * Determines whether or not line intersects this Extent. This method may be faster than + * checking the size of the array returned by intersect(Line). Implementing methods must ensure that + * this method returns true if and only if intersect(Line) returns a non-null array containing at least + * one element. + * + * @param line the Line with which to test for intersection. + * + * @return true if an intersection is found, false otherwise. + */ + boolean intersects(Line line); + + /** + * Calculate whether or not this Extent is intersected by plane. + * + * @param plane the Plane with which to test for intersection. + * + * @return true if plane is found to intersect this Extent. + */ + boolean intersects(Plane plane); } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/geom/ExtentHolder.java b/WorldWindAndroid/src/gov/nasa/worldwind/geom/ExtentHolder.java new file mode 100644 index 0000000..29fd0ce --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/geom/ExtentHolder.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.geom; + +import gov.nasa.worldwind.globes.Globe; + +/** + * ExtentHolder provides an interface to query an object's enclosing volume in model coordinates. + * + * @author dcollins + * @version $Id: ExtentHolder.java 1171 2013-02-11 21:45:02Z dcollins $ + * @see Extent + */ +public interface ExtentHolder +{ + /** + * Returns the objects enclosing volume as an {@link Extent} in model coordinates, given a + * specified {@link gov.nasa.worldwind.globes.Globe} and vertical exaggeration (see {@link + * gov.nasa.worldwind.SceneController#getVerticalExaggeration()}. + * + * @param globe the Globe the object is related to. + * @param verticalExaggeration the vertical exaggeration of the scene containing this object. + * + * @return the object's Extent in model coordinates. + * + * @throws IllegalArgumentException if the Globe is null. + */ + Extent getExtent(Globe globe, double verticalExaggeration); +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/geom/Frustum.java b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Frustum.java index d11c4da..018c66d 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/geom/Frustum.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Frustum.java @@ -294,6 +294,55 @@ public Frustum setPerspective(Angle horizontalFieldOfView, double viewportWidth, return this; } + /** + * Creates a Frustum from four edge vectors, viewport aspect ratio and distance to near and far planes. + * The edge vectors connect the near corners of the frustum to the far corners. The near plane must be closer than + * the far plane, and both planes must be positive. + * + * @param vTL vector defining the top-left of the frustum + * @param vTR vector defining the top-right of the frustum + * @param vBL vector defining the bottom-left of the frustum + * @param vBR vector defining the bottom-right of the frustum + * @param near distance to the near plane + * @param far distance to far plane + * + * @return Frustum that was created + * + * @throws IllegalArgumentException if any of the vectors are null, if either near or far are negative, or near is + * greater than or equal to far + */ + public static Frustum fromPerspectiveVecs(Vec4 vTL, Vec4 vTR, Vec4 vBL, Vec4 vBR, + double near, double far) + { + if (vTL == null || vTR == null || vBL == null || vBR == null) + { + String message = Logging.getMessage("Geom.ViewFrustum.EdgeVectorIsNull"); + Logging.verbose(message); + throw new IllegalArgumentException(message); + } + + double farMinusNear = far - near; + if (near <= 0 || farMinusNear <= 0) + { + String message = Logging.getMessage("Geom.ViewFrustum.ClippingDistanceOutOfRange"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + Vec4 lpn = vBL.cross3(vTL).normalize3(); + Plane leftPlane = new Plane(lpn.x, lpn.y, lpn.z, 0); + Vec4 rpn = vTR.cross3(vBR).normalize3(); + Plane rightPlane = new Plane(rpn.x, rpn.y, rpn.z, 0); + Vec4 bpn = vBR.cross3(vBL).normalize3(); + Plane bottomPlane = new Plane(bpn.x, bpn.y, bpn.z, 0); + Vec4 tpn = vTL.cross3(vTR).normalize3(); + Plane topPlane = new Plane(tpn.x, tpn.y, tpn.z, 0); + + Plane nearPlane = new Plane(0d, 0d, 0d - 1d, 0d - near); + Plane farPlane = new Plane(0d, 0d, 1d, far); + return new Frustum(leftPlane, rightPlane, bottomPlane, topPlane, nearPlane, farPlane); + } + /** * Returns the left plane. * @@ -385,6 +434,46 @@ public boolean intersects(Extent extent) return extent.intersects(this); } + /** + * Determines whether a line segment intersects this frustum. + * + * @param pa one end of the segment. + * @param pb the other end of the segment. + * + * @return true if the segment intersects or is contained in the frustum, otherwise false. + * + * @throws IllegalArgumentException if either point is null. + */ + public boolean intersectsSegment(Vec4 pa, Vec4 pb) + { + if (pa == null || pb == null) + { + String message = Logging.getMessage("nullValue.PointIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // First do a trivial accept test. + if (this.contains(pa) || this.contains(pb)) + return true; + + if (pa.equals(pb)) + return false; + + for (Plane p : this.getAllPlanes()) + { + // See if both points are behind the plane and therefore not in the frustum. + if (p.onSameSide(pa, pb) < 0) + return false; + + // See if the segment intersects the plane. + if (p.clip(pa, pb) != null) + return true; + } + + return false; // segment does not intersect frustum + } + /** * Indicates whether a specified point is within this frustum. * diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/geom/LatLon.java b/WorldWindAndroid/src/gov/nasa/worldwind/geom/LatLon.java index 064e48f..2174c22 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/geom/LatLon.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/geom/LatLon.java @@ -4,6 +4,8 @@ */ package gov.nasa.worldwind.geom; +import android.os.Parcel; +import android.os.Parcelable; import gov.nasa.worldwind.util.Logging; /** @@ -12,7 +14,9 @@ * @author dcollins * @version $Id: LatLon.java 812 2012-09-26 22:03:40Z dcollins $ */ -public class LatLon { +public class LatLon implements Parcelable { + public static final LatLon ZERO = new LatLon(Angle.ZERO, Angle.ZERO); + public final Angle latitude; public final Angle longitude; @@ -38,6 +42,27 @@ public LatLon(Angle latitude, Angle longitude) { this.longitude = longitude; } + /** + * Obtains the latitude of this LatLon. + * + * @return this LatLon's latitude + */ + public final Angle getLatitude() + { + return this.latitude; + } + + /** + * Obtains the longitude of this LatLon. + * + * @return this LatLon's longitude + */ + public final Angle getLongitude() + { + return this.longitude; + } + + public static LatLon fromDegrees(double latitude, double longitude) { return new LatLon(Angle.fromDegrees(latitude), Angle.fromDegrees(longitude)); } @@ -46,6 +71,79 @@ public static LatLon fromRadians(double latitude, double longitude) { return new LatLon(Angle.fromRadians(latitude), Angle.fromRadians(longitude)); } + public LatLon add(LatLon that) + { + if (that == null) + { + String msg = Logging.getMessage("nullValue.AngleIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + Angle lat = Angle.normalizedLatitude(this.latitude.add(that.latitude)); + Angle lon = Angle.normalizedLongitude(this.longitude.add(that.longitude)); + + return new LatLon(lat, lon); + } + + public LatLon subtract(LatLon that) + { + if (that == null) + { + String msg = Logging.getMessage("nullValue.AngleIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + Angle lat = Angle.normalizedLatitude(this.latitude.subtract(that.latitude)); + Angle lon = Angle.normalizedLongitude(this.longitude.subtract(that.longitude)); + + return new LatLon(lat, lon); + } + + /** + * Returns the linear interpolation of value1 and value2, treating the geographic + * locations as simple 2D coordinate pairs. + * + * @param amount the interpolation factor + * @param value1 the first location. + * @param value2 the second location. + * + * @return the linear interpolation of value1 and value2. + * + * @throws IllegalArgumentException if either location is null. + */ + public static LatLon interpolate(double amount, LatLon value1, LatLon value2) + { + if (value1 == null || value2 == null) + { + String message = Logging.getMessage("nullValue.LatLonIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (value1.equals(value2)) + return value1; + + Line line; + try + { + line = Line.fromSegment( + new Vec4(value1.getLongitude().radians, value1.getLatitude().radians, 0), + new Vec4(value2.getLongitude().radians, value2.getLatitude().radians, 0)); + } + catch (IllegalArgumentException e) + { + // Locations became coincident after calculations. + return value1; + } + + Vec4 p = new Vec4(); + line.getPointAt(amount, p); + + return LatLon.fromRadians(p.y, p.x); + } + /** * Returns the an interpolated location along the great-arc between the specified locations. This does not retain * any reference to the specified locations, or modify them in any way. @@ -267,6 +365,28 @@ public static LatLon greatCircleEndPosition(LatLon location, Angle greatCircleAz Angle.normalizedDegreesLongitude(Angle.fromRadians(endLonRadians).degrees)); } + /** + * Computes the location on a great circle arc with the given starting location, azimuth, and arc distance. + * + * @param p LatLon of the starting location + * @param greatCircleAzimuthRadians great circle azimuth angle (clockwise from North), in radians + * @param pathLengthRadians arc distance to travel, in radians + * + * @return LatLon location on the great circle arc. + */ + public static LatLon greatCircleEndPosition(LatLon p, double greatCircleAzimuthRadians, double pathLengthRadians) + { + if (p == null) + { + String message = Logging.getMessage("nullValue.LatLonIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + return greatCircleEndPosition(p, + Angle.fromRadians(greatCircleAzimuthRadians), Angle.fromRadians(pathLengthRadians)); + } + public static boolean locationsCrossDateline(LatLon p1, LatLon p2) { if (p1 == null || p2 == null) { String msg = Logging.getMessage("nullValue.LocationIsNull"); @@ -284,6 +404,35 @@ public static boolean locationsCrossDateline(LatLon p1, LatLon p2) { return false; } + public static boolean locationsCrossDateLine(Iterable locations) + { + if (locations == null) + { + String msg = Logging.getMessage("nullValue.LocationsListIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + LatLon pos = null; + for (LatLon posNext : locations) + { + if (pos != null) + { + // A segment cross the line if end pos have different longitude signs + // and are more than 180 degrees longitude apart + if (Math.signum(pos.getLongitude().degrees) != Math.signum(posNext.getLongitude().degrees)) + { + double delta = Math.abs(pos.getLongitude().degrees - posNext.getLongitude().degrees); + if (delta > 180 && delta < 360) + return true; + } + } + pos = posNext; + } + + return false; + } + /** * Computes the azimuth angle (clockwise from North) of a rhumb line (a line of constant heading) between two * locations. This does not retain any reference to the specified locations, or modify them in any way. @@ -328,6 +477,197 @@ public static Angle rhumbAzimuth(LatLon lhs, LatLon rhs) { return Double.isNaN(azimuthRadians) ? Angle.fromRadians(0) : Angle.fromRadians(azimuthRadians); } + /** + * Returns two locations with the most extreme latitudes on the great circle with the given starting location and + * azimuth. + * + * @param location location on the great circle. + * @param azimuth great circle azimuth angle (clockwise from North). + * + * @return two locations where the great circle has its extreme latitudes. + * + * @throws IllegalArgumentException if either location or azimuth are null. + */ + public static LatLon[] greatCircleExtremeLocations(LatLon location, Angle azimuth) + { + if (location == null) + { + String message = Logging.getMessage("nullValue.LocationIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (azimuth == null) + { + String message = Logging.getMessage("nullValue.AzimuthIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + double lat0 = location.getLatitude().radians; + double az = azimuth.radians; + + // Derived by solving the function for longitude on a great circle against the desired longitude. We start with + // the equation in "Map Projections - A Working Manual", page 31, equation 5-5: + // + // lat = asin( sin(lat0) * cos(c) + cos(lat0) * sin(c) * cos(Az) ) + // + // Where (lat0, lon) are the starting coordinates, c is the angular distance along the great circle from the + // starting coordinate, and Az is the azimuth. All values are in radians. + // + // Solving for angular distance gives distance to the equator: + // + // tan(c) = -tan(lat0) / cos(Az) + // + // The great circle is by definition centered about the Globe's origin. Therefore intersections with the + // equator will be antipodal (exactly 180 degrees opposite each other), as will be the extreme latitudes. + // By observing the symmetry of a great circle, it is also apparent that the extreme latitudes will be 90 + // degrees from either intersection with the equator. + // + // d1 = c + 90 + // d2 = c - 90 + + double tanDistance = -Math.tan(lat0) / Math.cos(az); + double distance = Math.atan(tanDistance); + + Angle extremeDistance1 = Angle.fromRadians(distance + (Math.PI / 2.0)); + Angle extremeDistance2 = Angle.fromRadians(distance - (Math.PI / 2.0)); + + return new LatLon[] + { + greatCircleEndPosition(location, azimuth, extremeDistance1), + greatCircleEndPosition(location, azimuth, extremeDistance2) + }; + } + + /** + * Returns two locations with the most extreme latitudes on the great circle arc defined by, and limited to, the two + * locations. + * + * @param begin beginning location on the great circle arc. + * @param end ending location on the great circle arc. + * + * @return two locations with the most extreme latitudes on the great circle arc. + * + * @throws IllegalArgumentException if either begin or end are null. + */ + public static LatLon[] greatCircleArcExtremeLocations(LatLon begin, LatLon end) + { + if (begin == null) + { + String message = Logging.getMessage("nullValue.BeginIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (end == null) + { + String message = Logging.getMessage("nullValue.EndIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + LatLon minLatLocation = null; + LatLon maxLatLocation = null; + double minLat = Angle.POS90.degrees; + double maxLat = Angle.NEG90.degrees; + + // Compute the min and max latitude and associated locations from the arc endpoints. + for (LatLon ll : java.util.Arrays.asList(begin, end)) + { + if (minLat >= ll.getLatitude().degrees) + { + minLat = ll.getLatitude().degrees; + minLatLocation = ll; + } + if (maxLat <= ll.getLatitude().degrees) + { + maxLat = ll.getLatitude().degrees; + maxLatLocation = ll; + } + } + + // Compute parameters for the great circle arc defined by begin and end. Then compute the locations of extreme + // latitude on entire the great circle which that arc is part of. + Angle greatArcAzimuth = greatCircleAzimuth(begin, end); + Angle greatArcDistance = greatCircleDistance(begin, end); + LatLon[] greatCircleExtremes = greatCircleExtremeLocations(begin, greatArcAzimuth); + + // Determine whether either of the extreme locations are inside the arc defined by begin and end. If so, + // adjust the min and max latitude accordingly. + for (LatLon ll : greatCircleExtremes) + { + Angle az = LatLon.greatCircleAzimuth(begin, ll); + Angle d = LatLon.greatCircleDistance(begin, ll); + + // The extreme location must be between the begin and end locations. Therefore its azimuth relative to + // the begin location should have the same signum, and its distance relative to the begin location should + // be between 0 and greatArcDistance, inclusive. + if (Math.signum(az.degrees) == Math.signum(greatArcAzimuth.degrees)) + { + if (d.degrees >= 0 && d.degrees <= greatArcDistance.degrees) + { + if (minLat >= ll.getLatitude().degrees) + { + minLat = ll.getLatitude().degrees; + minLatLocation = ll; + } + if (maxLat <= ll.getLatitude().degrees) + { + maxLat = ll.getLatitude().degrees; + maxLatLocation = ll; + } + } + } + } + + return new LatLon[] {minLatLocation, maxLatLocation}; + } + + /** + * Returns two locations with the most extreme latitudes on the sequence of great circle arcs defined by each pair + * of locations in the specified iterable. + * + * @param locations the pairs of locations defining a sequence of great circle arcs. + * + * @return two locations with the most extreme latitudes on the great circle arcs. + * + * @throws IllegalArgumentException if locations is null. + */ + public static LatLon[] greatCircleArcExtremeLocations(Iterable locations) + { + if (locations == null) + { + String message = Logging.getMessage("nullValue.LocationsListIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + LatLon minLatLocation = null; + LatLon maxLatLocation = null; + + LatLon lastLocation = null; + + for (LatLon ll : locations) + { + if (lastLocation != null) + { + LatLon[] extremes = LatLon.greatCircleArcExtremeLocations(lastLocation, ll); + if (extremes == null) + continue; + + if (minLatLocation == null || minLatLocation.getLatitude().degrees > extremes[0].getLatitude().degrees) + minLatLocation = extremes[0]; + if (maxLatLocation == null || maxLatLocation.getLatitude().degrees < extremes[1].getLatitude().degrees) + maxLatLocation = extremes[1]; + } + + lastLocation = ll; + } + + return new LatLon[] {minLatLocation, maxLatLocation}; + } + /** * Computes the length of the rhumb line between two locations. The return value gives the distance as the angular * distance between the two positions on the pi radius circle. In radians, this angle is also the arc length of the @@ -441,6 +781,28 @@ public static LatLon rhumbEndPosition(LatLon location, Angle rhumbAzimuth, Angle return LatLon.fromDegrees(Angle.normalizedDegreesLatitude(Angle.fromRadians(lat2).degrees), Angle.normalizedDegreesLongitude(Angle.fromRadians(lon2).degrees)); } + /** + * Computes the location on a rhumb line with the given starting location, rhumb azimuth, and arc distance along the + * line. + * + * @param p LatLon of the starting location + * @param rhumbAzimuthRadians rhumb azimuth angle (clockwise from North), in radians + * @param pathLengthRadians arc distance to travel, in radians + * + * @return LatLon location on the rhumb line. + */ + public static LatLon rhumbEndPosition(LatLon p, double rhumbAzimuthRadians, double pathLengthRadians) + { + if (p == null) + { + String message = Logging.getMessage("nullValue.LatLonIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + return rhumbEndPosition(p, Angle.fromRadians(rhumbAzimuthRadians), Angle.fromRadians(pathLengthRadians)); + } + public LatLon copy() { return new LatLon(this.latitude.copy(), this.longitude.copy()); } @@ -517,4 +879,29 @@ public String toString() { sb.append(")"); return sb.toString(); } + + private static final ClassLoader LOADER = LatLon.class.getClassLoader(); + + public static final Creator CREATOR = new Creator() { + @Override + public LatLon createFromParcel(Parcel in) { + return new LatLon(in.readParcelable(LOADER), in.readParcelable(LOADER)); + } + + @Override + public LatLon[] newArray(int size) { + return new LatLon[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(this.latitude, flags); + dest.writeParcelable(this.longitude, flags); + } } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/geom/Line.java b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Line.java index 03d376a..8b85f60 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/geom/Line.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Line.java @@ -70,6 +70,23 @@ public static Vec4 nearestPointToSegment(Vec4 p0, Vec4 p1, Vec4 point) protected final Vec4 origin; protected final Vec4 direction; + /** + * Create the line containing a line segement between two points. + * + * @param pa the first point of the line segment. + * @param pb the second point of the line segment. + * + * @return The line containing the two points. + * + * @throws IllegalArgumentException if either point is null or they are coincident. + */ + public static Line fromSegment(Vec4 pa, Vec4 pb) + { + Line line = new Line(); + line.setSegment(pa, pb); + return line; + } + /** Creates a new line with its origin set to (0, 0, 0) and its direction set to (1, 0, 0). */ public Line() { @@ -231,7 +248,7 @@ public Vec4 getDirection() return this.direction; } - public void getPointAt(double t, Vec4 result) + public Vec4 getPointAt(double t, Vec4 result) { if (result == null) { @@ -241,6 +258,7 @@ public void getPointAt(double t, Vec4 result) } result.setPointOnLine3(this.origin, t, this.direction); + return result; } public double selfDot() @@ -297,6 +315,42 @@ public void nearestPointTo(Vec4 point, Vec4 result) result.z = this.origin.z + this.direction.z * c; } + /** + * Determine if a point is behind the Line's origin. + * + * @param point The point to test. + * + * @return true if point is behind this Line's origin, false otherwise. + */ + public boolean isPointBehindLineOrigin(Vec4 point) + { + double dot = point.subtract3(this.getOrigin()).dot3(this.getDirection()); + return dot < 0.0; + } + + public Vec4 nearestIntersectionPoint(Intersection[] intersections) + { + Vec4 intersectionPoint = null; + + // Find the nearest intersection that's in front of the ray origin. + double nearestDistance = Double.MAX_VALUE; + for (Intersection intersection : intersections) + { + // Ignore any intersections behind the line origin. + if (!this.isPointBehindLineOrigin(intersection.getIntersectionPoint())) + { + double d = intersection.getIntersectionPoint().distanceTo3(this.getOrigin()); + if (d < nearestDistance) + { + intersectionPoint = intersection.getIntersectionPoint(); + nearestDistance = d; + } + } + } + + return intersectionPoint; + } + /** * Performs a comparison to test whether this Object is internally identical to the other Object o. * This method takes into account both direction and origin, so two lines which may be equivalent may not be diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/geom/Matrix.java b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Matrix.java index b645e18..0c3448d 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/geom/Matrix.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Matrix.java @@ -5,6 +5,7 @@ */ package gov.nasa.worldwind.geom; +import android.graphics.PointF; import gov.nasa.worldwind.terrain.Terrain; import gov.nasa.worldwind.util.Logging; @@ -26,7 +27,32 @@ */ public class Matrix { + public static final Matrix IDENTITY = new Matrix( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1); + + //Matrix indices as row major notation (Row x Column) + public static final int M00 = 0; // 0; + public static final int M01 = 1; // 1; + public static final int M02 = 2; // 2; + public static final int M03 = 3; // 3; + public static final int M10 = 4; // 4; + public static final int M11 = 5; // 5; + public static final int M12 = 6; // 6; + public static final int M13 = 7; // 7; + public static final int M20 = 8; // 8; + public static final int M21 = 9; // 9; + public static final int M22 = 10; // 10; + public static final int M23 = 11; // 11; + public static final int M30 = 12; // 12; + public static final int M31 = 13; // 13; + public static final int M32 = 14; // 14; + public static final int M33 = 15; // 15; + public final double[] m = new double[16]; + // This is a temporary vector used to prevent allocating a point in order to compute cartesian points from // geographic positions in the setter methods below. protected Vec4 point; @@ -337,6 +363,486 @@ public static Matrix fromOrthographic(double left, double right, double bottom, return Matrix.fromIdentity().setOrthographic(left, right, bottom, top, near, far); } + public static Matrix fromOrthographic2D(double width, double height) + { + if (width <= 0.0) + { + String msg = Logging.getMessage("generic.ArgumentOutOfRange", width); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + if (height <= 0.0) + { + String msg = Logging.getMessage("generic.ArgumentOutOfRange", height); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + return new Matrix( + 2.0 / width, 0.0, 0.0, 0.0, + 0.0, 2.0 / height, 0.0, 0.0, + 0.0, 0.0, -1.0, 0.0, + 0.0, 0.0, 0.0, 1.0); + } + + /** + * Computes a Matrix that will map a aligned 2D grid coordinates to geographic coordinates in degrees. + * It is assumed that the destination grid is parallel with lines of latitude and longitude, and has its origin in + * the upper left hand corner. + * + * @param sector the grid sector. + * @param imageWidth the grid width. + * @param imageHeight the grid height. + * + * @return Matrix that will map from grid coordinates to geographic coordinates in degrees. + * + * @throws IllegalArgumentException if sector is null, or if either width or + * height are less than 1. + */ + public static Matrix fromImageToGeographic(int imageWidth, int imageHeight, Sector sector) + { + if (imageWidth < 1 || imageHeight < 1) + { + String message = Logging.getMessage("generic.InvalidImageSize", imageWidth, imageHeight); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (sector == null) + { + String message = Logging.getMessage("nullValue.SectorIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // Transform from grid coordinates to geographic coordinates. Since the grid is parallel with lines of latitude + // and longitude, this is a simple scale and translation. + + double sx = sector.getDeltaLonDegrees() / imageWidth; + double sy = -sector.getDeltaLatDegrees() / imageHeight; + double tx = sector.minLongitude.degrees; + double ty = sector.maxLatitude.degrees; + + return new Matrix( + sx, 0.0, tx, 0.0, + 0.0, sy, ty, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 0.0); + } + +// public static Matrix fromImageToGeographic(AVList worldFileParams) +// { +// if (worldFileParams == null) +// { +// String message = Logging.getMessage("nullValue.ParamsIsNull"); +// Logging.error(message); +// throw new IllegalArgumentException(message); +// } +// +// // Transform from geographic coordinates to source grid coordinates. Start with the following system of +// // equations. The values a-f are defined by the world file, which construct and affine transform mapping grid +// // coordinates to geographic coordinates. We can simply plug these into the upper 3x3 values of our matrix. +// // +// // | a b c | | x | | lon | +// // | d e f | * | y | = | lat | +// // | 0 0 1 | | 1 | | 1 | +// +// Double a = AVListImpl.getDoubleValue(worldFileParams, WorldFile.WORLD_FILE_X_PIXEL_SIZE); +// Double d = AVListImpl.getDoubleValue(worldFileParams, WorldFile.WORLD_FILE_Y_COEFFICIENT); +// Double b = AVListImpl.getDoubleValue(worldFileParams, WorldFile.WORLD_FILE_X_COEFFICIENT); +// Double e = AVListImpl.getDoubleValue(worldFileParams, WorldFile.WORLD_FILE_Y_PIXEL_SIZE); +// Double c = AVListImpl.getDoubleValue(worldFileParams, WorldFile.WORLD_FILE_X_LOCATION); +// Double f = AVListImpl.getDoubleValue(worldFileParams, WorldFile.WORLD_FILE_Y_LOCATION); +// +// if (a == null || b == null || c == null || d == null || e == null || f == null) +// { +// return null; +// } +// +// return new Matrix( +// a, b, c, 0.0, +// d, e, f, 0.0, +// 0.0, 0.0, 1.0, 0.0, +// 0.0, 0.0, 0.0, 0.0); +// } +// +// public static Matrix fromGeographicToImage(AVList worldFileParams) +// { +// if (worldFileParams == null) +// { +// String message = Logging.getMessage("nullValue.ParamsIsNull"); +// Logging.error(message); +// throw new IllegalArgumentException(message); +// } +// +// // Transform from geographic coordinates to source grid coordinates. Start with the following system of +// // equations. The values a-f are defined by the world file, which construct and affine transform mapping grid +// // coordinates to geographic coordinates. We want to find the transform that maps geographic coordinates to +// // grid coordinates. +// // +// // | a b c | | x | | lon | +// // | d e f | * | y | = | lat | +// // | 0 0 1 | | 1 | | 1 | +// // +// // Expanding the matrix multiplication: +// // +// // a*x + b*y + c = lon +// // d*x + e*y + f = lat +// // +// // Then solving for x and y by eliminating variables: +// // +// // x0 = d - (e*a)/b +// // y0 = e - (d*b)/a +// // (-e/(b*x0))*lon + (1/x0)*lat + (e*c)/(b*x0) - f/x0 = x +// // (-d/(a*y0))*lon + (1/y0)*lat + (d*c)/(a*y0) - f/y0 = y +// // +// // And extracting new the matrix coefficients a'-f': +// // +// // a' = -e/(b*x0) +// // b' = 1/x0 +// // c' = (e*c)/(b*x0) - f/x0 +// // d' = -d/(a*y0) +// // e' = 1/y0 +// // f' = (d*c)/(a*y0) - f/y0 +// // +// // If b==0 and d==0, then we have the equation simplifies to: +// // +// // (1/a)*lon + (-c/a) = x +// // (1/e)*lat + (-f/e) = y +// // +// // And and the new matrix coefficients will be: +// // +// // a' = 1/a +// // b' = 0 +// // c' = -c/a +// // d' = 0 +// // e' = 1/e +// // f' = -f/e +// +// Double a = AVListImpl.getDoubleValue(worldFileParams, WorldFile.WORLD_FILE_X_PIXEL_SIZE); +// Double d = AVListImpl.getDoubleValue(worldFileParams, WorldFile.WORLD_FILE_Y_COEFFICIENT); +// Double b = AVListImpl.getDoubleValue(worldFileParams, WorldFile.WORLD_FILE_X_COEFFICIENT); +// Double e = AVListImpl.getDoubleValue(worldFileParams, WorldFile.WORLD_FILE_Y_PIXEL_SIZE); +// Double c = AVListImpl.getDoubleValue(worldFileParams, WorldFile.WORLD_FILE_X_LOCATION); +// Double f = AVListImpl.getDoubleValue(worldFileParams, WorldFile.WORLD_FILE_Y_LOCATION); +// +// if (a == null || b == null || c == null || d == null || e == null || f == null) +// { +// return null; +// } +// +// if (b == 0.0 && d == 0.0) +// { +// return new Matrix( +// 1.0 / a, 0.0, (-c / a), 0.0, +// 0.0, 1.0 / e, (-f / e), 0.0, +// 0.0, 0.0, 1.0, 0.0, +// 0.0, 0.0, 0.0, 0.0); +// } +// else +// { +// double x0 = d - (e * a) / b; +// double ap = -e / (b * x0); +// double bp = 1.0 / x0; +// double cp = (e * c) / (b * x0) - f / x0; +// +// double y0 = e - (d * b) / a; +// double dp = -d / (a * y0); +// double ep = 1.0 / y0; +// double fp = (d * c) / (a * y0) - f / y0; +// +// return new Matrix( +// ap, bp, cp, 0.0, +// dp, ep, fp, 0.0, +// 0.0, 0.0, 1.0, 0.0, +// 0.0, 0.0, 0.0, 0.0); +// } +// } + + /** + * Computes a Matrix that will map constrained 2D grid coordinates to geographic coordinates in + * degrees. The grid is defined by three control points. Each control point maps a location in the source grid to a + * geographic location. + * + * @param imagePoints three control points in the source grid. + * @param geoPoints three geographic locations corresponding to each grid control point. + * + * @return Matrix that will map from geographic coordinates to grid coordinates in degrees. + * + * @throws IllegalArgumentException if either imagePoints or geoPoints is null or have + * length less than 3. + */ + public static Matrix fromImageToGeographic(PointF[] imagePoints, LatLon[] geoPoints) + { + if (imagePoints == null) + { + String message = Logging.getMessage("nullValue.ImagePointsIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (geoPoints == null) + { + String message = Logging.getMessage("nullValue.GeoPointsIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (imagePoints.length < 3) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "imagePoints.length < 3"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (geoPoints.length < 3) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "geoPoints.length < 3"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // Transform from geographic coordinates to source grid coordinates. Start with the following system of + // equations. The values a-f are the unknown coefficients we want to derive, The (lat,lon) and (x,y) + // coordinates are constants defined by the caller via geoPoints and imagePoints, respectively. + // + // | a b c | | x1 x2 x3 | | lon1 lon2 lon3 | + // | d e f | * | y1 y2 y3 | = | lat1 lat2 lat3 | + // | 0 0 1 | | 1 1 1 | | 1 1 1 | + // + // Expanding the matrix multiplication: + // + // a*x1 + b*y1 + c = lon1 + // a*x2 + b*y2 + c = lon2 + // a*x3 + b*y3 + c = lon3 + // d*x1 + e*y1 + f = lat1 + // d*x2 + e*y2 + f = lat2 + // d*x3 + e*y3 + f = lat3 + // + // Then solving for a-c, and d-f by repeatedly eliminating variables: + // + // a0 = (x3-x1) - (x2-x1)*(y3-y1)/(y2-y1) + // a = (1/a0) * [(lon3-lon1) - (lon2-lon1)*(y3-y1)/(y2-y1)] + // b = (lon2-lon1)/(y2-y1) - a*(x2-x1)/(y2-y1) + // c = lon1 - a*x1 - b*y1 + // + // d0 = (x3-x1) - (x2-x1)*(y3-y1)/(y2-y1) + // d = (1/d0) * [(lat3-lat1) - (lat2-lat1)*(y3-y1)/(y2-y1)] + // e = (lat2-lat1)/(y2-y1) - d*(x2-x1)/(y2-y1) + // f = lat1 - d*x1 - e*y1 + + double lat1 = geoPoints[0].getLatitude().degrees; + double lat2 = geoPoints[1].getLatitude().degrees; + double lat3 = geoPoints[2].getLatitude().degrees; + double lon1 = geoPoints[0].getLongitude().degrees; + double lon2 = geoPoints[1].getLongitude().degrees; + double lon3 = geoPoints[2].getLongitude().degrees; + + double x1 = imagePoints[0].x; + double x2 = imagePoints[1].x; + double x3 = imagePoints[2].x; + double y1 = imagePoints[0].y; + double y2 = imagePoints[1].y; + double y3 = imagePoints[2].y; + + double a0 = (x3 - x1) - (x2 - x1) * (y3 - y1) / (y2 - y1); + double a = (1 / a0) * ((lon3 - lon1) - (lon2 - lon1) * (y3 - y1) / (y2 - y1)); + double b = (lon2 - lon1) / (y2 - y1) - a * (x2 - x1) / (y2 - y1); + double c = lon1 - a * x1 - b * y1; + + double d0 = (x3 - x1) - (x2 - x1) * (y3 - y1) / (y2 - y1); + double d = (1 / d0) * ((lat3 - lat1) - (lat2 - lat1) * (y3 - y1) / (y2 - y1)); + double e = (lat2 - lat1) / (y2 - y1) - d * (x2 - x1) / (y2 - y1); + double f = lat1 - d * x1 - e * y1; + + return new Matrix( + a, b, c, 0.0, + d, e, f, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 0.0); + } + + public static Matrix fromGeographicToImage(PointF[] imagePoints, LatLon[] geoPoints) + { + if (imagePoints == null) + { + String message = Logging.getMessage("nullValue.ImagePointsIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (geoPoints == null) + { + String message = Logging.getMessage("nullValue.GeoPointsIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (imagePoints.length < 3) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "imagePoints.length < 3"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (geoPoints.length < 3) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "geoPoints.length < 3"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // Transform from geographic coordinates to source grid coordinates. Start with the following system of + // equations. The values a-f are the unknown coefficients we want to derive, The (lat,lon) and (x,y) + // coordinates are constants defined by the caller via geoPoints and imagePoints, respectively. + // + // | a b c | | lon1 lon2 lon3 | | x1 x2 x3 | + // | d e f | * | lat1 lat2 lat3 | = | y1 y2 y3 | + // | 0 0 1 | | 1 1 1 | | 1 1 1 | + // + // Expanding the matrix multiplication: + // + // a*lon1 + b*lat1 + c = x1 + // a*lon2 + b*lat2 + c = x2 + // a*lon3 + b*lat3 + c = x3 + // d*lon1 + e*lat1 + f = y1 + // d*lon2 + e*lat2 + f = y2 + // d*lon3 + e*lat3 + f = y3 + // + // Then solving for a-c, and d-f by repeatedly eliminating variables: + // + // a0 = (lon3-lon1) - (lon2-lon1)*(lat3-lat1)/(lat2-lat1) + // a = (1/a0) * [(x3-x1) - (x2-x1)*(lat3-lat1)/(lat2-lat1)] + // b = (x2-x1)/(lat2-lat1) - a*(lon2-lon1)/(lat2-lat1) + // c = x1 - a*lon1 - b*lat1 + // + // d0 = (lon3-lon1) - (lon2-lon1)*(lat3-lat1)/(lat2-lat1) + // d = (1/d0) * [(y3-y1) - (y2-y1)*(lat3-lat1)/(lat2-lat1)] + // e = (y2-y1)/(lat2-lat1) - d*(lon2-lon1)/(lat2-lat1) + // f = y1 - d*lon1 - e*lat1 + + double lat1 = geoPoints[0].getLatitude().degrees; + double lat2 = geoPoints[1].getLatitude().degrees; + double lat3 = geoPoints[2].getLatitude().degrees; + double lon1 = geoPoints[0].getLongitude().degrees; + double lon2 = geoPoints[1].getLongitude().degrees; + double lon3 = geoPoints[2].getLongitude().degrees; + + double x1 = imagePoints[0].x; + double x2 = imagePoints[1].x; + double x3 = imagePoints[2].x; + double y1 = imagePoints[0].y; + double y2 = imagePoints[1].y; + double y3 = imagePoints[2].y; + + double a0 = (lon3 - lon1) - (lon2 - lon1) * (lat3 - lat1) / (lat2 - lat1); + double a = (1 / a0) * ((x3 - x1) - (x2 - x1) * (lat3 - lat1) / (lat2 - lat1)); + double b = (x2 - x1) / (lat2 - lat1) - a * (lon2 - lon1) / (lat2 - lat1); + double c = x1 - a * lon1 - b * lat1; + + double d0 = (lon3 - lon1) - (lon2 - lon1) * (lat3 - lat1) / (lat2 - lat1); + double d = (1 / d0) * ((y3 - y1) - (y2 - y1) * (lat3 - lat1) / (lat2 - lat1)); + double e = (y2 - y1) / (lat2 - lat1) - d * (lon2 - lon1) / (lat2 - lat1); + double f = y1 - d * lon1 - e * lat1; + + return new Matrix( + a, b, c, 0.0, + d, e, f, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 0.0); + } + + /** + * Computes a Matrix that will map the geographic region defined by sector onto a Cartesian region of the specified + * width and height and centered at the point (x, y). + * + * @param sector the geographic region which will be mapped to the Cartesian region + * @param x x-coordinate of lower left hand corner of the Cartesian region + * @param y y-coordinate of lower left hand corner of the Cartesian region + * @param width width of the Cartesian region, extending to the right from the x-coordinate + * @param height height of the Cartesian region, extending up from the y-coordinate + * + * @return Matrix that will map from the geographic region to the Cartesian region. + * + * @throws IllegalArgumentException if sector is null, or if width or height + * are less than zero. + */ + public static Matrix fromGeographicToViewport(Sector sector, int x, int y, int width, int height) + { + if (sector == null) + { + String message = Logging.getMessage("nullValue.SectorIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (width <= 0) + { + String message = Logging.getMessage("Geom.WidthInvalid", width); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (height <= 0) + { + String message = Logging.getMessage("Geom.HeightInvalid", height); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + Matrix transform = Matrix.IDENTITY; + transform = transform.multiply( + Matrix.fromTranslation(-x, -y, 0.0)); + transform = transform.multiply( + Matrix.fromScale(width / sector.getDeltaLonDegrees(), height / sector.getDeltaLatDegrees(), 1.0)); + transform = transform.multiply( + Matrix.fromTranslation(-sector.minLongitude.degrees, -sector.minLatitude.degrees, 0.0)); + + return transform; + } + + /** + * Computes a Matrix that will map a Cartesian region of the specified width and height + * and centered at the point (x, y) to the geographic region defined by sector onto . + * + * @param sector the geographic region the Cartesian region will be mapped to + * @param x x-coordinate of lower left hand corner of the Cartesian region + * @param y y-coordinate of lower left hand corner of the Cartesian region + * @param width width of the Cartesian region, extending to the right from the x-coordinate + * @param height height of the Cartesian region, extending up from the y-coordinate + * + * @return Matrix that will map from Cartesian region to the geographic region. + * + * @throws IllegalArgumentException if sector is null, or if width or height + * are less than zero. + */ + public static Matrix fromViewportToGeographic(Sector sector, int x, int y, int width, int height) + { + if (sector == null) + { + String message = Logging.getMessage("nullValue.SectorIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (width <= 0) + { + String message = Logging.getMessage("Geom.WidthInvalid", width); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (height <= 0) + { + String message = Logging.getMessage("Geom.HeightInvalid", height); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + Matrix transform = Matrix.IDENTITY; + transform = transform.multiply( + Matrix.fromTranslation(sector.minLongitude.degrees, sector.minLatitude.degrees, 0.0)); + transform = transform.multiply( + Matrix.fromScale(sector.getDeltaLonDegrees() / width, sector.getDeltaLatDegrees() / height, 1.0)); + transform = transform.multiply( + Matrix.fromTranslation(x, y, 0.0)); + + return transform; + } + /** * Computes a a symmetric covariance Matrix from the x, y, z coordinates of the specified points Iterable. This does * not retain any reference to the specified iterable or its vectors, nor does this modify the vectors in any way. @@ -619,6 +1125,25 @@ public Matrix set(Matrix matrix) return this; } + /** + * Sets the four columns of this {@link Matrix} which correspond to the x-, y-, and z- + * axis of the vector space this {@link Matrix} creates as well as the 4th column representing + * the translation of any point that is multiplied by this {@link Matrix}. + * + * @param xAxis {@link Vec4} The x axis. + * @param yAxis {@link Vec4} The y axis. + * @param zAxis {@link Vec4} The z axis. + * @param pos {@link Vec4} The translation vector. + * @return A reference to this {@link Matrix} to facilitate chaining. + */ + public Matrix setAll(final Vec4 xAxis, final Vec4 yAxis, final Vec4 zAxis, final Vec4 pos) { + m[M00] = xAxis.x; m[M01] = yAxis.x; m[M02] = zAxis.x; m[M03] = pos.x; + m[M10] = xAxis.y; m[M11] = yAxis.y; m[M12] = zAxis.y; m[M13] = pos.y; + m[M20] = xAxis.z; m[M21] = yAxis.z; m[M22] = zAxis.z; m[M23] = pos.z; + m[M30] = 0; m[M31] = 0; m[M32] = 0; m[M33] = 1; + return this; + } + public Matrix set(double[] array, int offset) { if (array == null) diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/geom/MeasurableArea.java b/WorldWindAndroid/src/gov/nasa/worldwind/geom/MeasurableArea.java new file mode 100644 index 0000000..9087e19 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/geom/MeasurableArea.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ + +package gov.nasa.worldwind.geom; + +import gov.nasa.worldwind.globes.Globe; + +/** + * This interfaces provides methods to query measurements of surface-defining objects. These methods all require a Globe + * parameter in order to compute their spatial location, and for terrain-confoming objects, the terrain elevations. + * + * @author tag + * @version $Id: MeasurableArea.java 1171 2013-02-11 21:45:02Z dcollins $ + */ +public interface MeasurableArea +{ + /** + * Returns the object's area in square meters. If the object conforms to terrain, the area returned is the surface + * area of the terrain, including its hillsides and other undulations. + * + * @param globe The globe the object is related to. + * @return the object's area in square meters. Returns -1 if the object does not form an area due to an insufficient + * number of vertices or any other condition. + * @throws IllegalArgumentException if the globe is null. + */ + double getArea(Globe globe); + + /** + * Returns the length of the object's perimeter in meters. If the object conforms to terrain, the perimeter is that + * along the terrain, including its hillsides and other undulations. + * + * @param globe The globe the object is related to. + * @return the object's perimeter in meters. Returns -1 if the object does not form an area due to an insufficient + * number of vertices or any other condition. + * @throws IllegalArgumentException if the globe is null. + */ + double getPerimeter(Globe globe); + + /** + * Returns the longitudinal length of the object in meters. The length is the distance from the object's west-most + * point to its east-most. If the object is terrain conforming then the + * + * @param globe The globe the object is related to. + * @return the width of the object in meters. + * @throws IllegalArgumentException if the globe is null. + */ + double getWidth(Globe globe); + + /** + * Returns the latitudanl length of the object in meters. The length is the distance from the objects south-most + * point to its east-most position. + * + * @param globe The globe the object is related to. + * @return the height of the object in meters. + * @throws IllegalArgumentException if the globe is null. + */ + double getHeight(Globe globe); +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/geom/MeasurableLength.java b/WorldWindAndroid/src/gov/nasa/worldwind/geom/MeasurableLength.java new file mode 100644 index 0000000..1f60745 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/geom/MeasurableLength.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ + +package gov.nasa.worldwind.geom; + +import gov.nasa.worldwind.globes.Globe; + +/** + * @author tag + * @version $Id: MeasurableLength.java 1171 2013-02-11 21:45:02Z dcollins $ + */ +public interface MeasurableLength +{ + /** + * Returns the object's length in meters. If the object conforms to terrain, the length is that along the terrain, + * including its hillsides and other undulations. + * + * @param globe The globe the object is related to. + * @return the object's length in meters. + * @throws IllegalArgumentException if the globe is null. + */ + double getLength(Globe globe); +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/geom/PickPointFrustum.java b/WorldWindAndroid/src/gov/nasa/worldwind/geom/PickPointFrustum.java new file mode 100644 index 0000000..18f38e8 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/geom/PickPointFrustum.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ + +package gov.nasa.worldwind.geom; + +import android.graphics.Point; +import gov.nasa.worldwind.util.Logging; + +/** + * A viewport aligned {@link Frustum} that also stores the 2D screen rectangle that the {@link + * Frustum} contains. + * + * @author Jeff Addison + * @version $Id$ + */ +public class PickPointFrustum extends Frustum +{ + private final android.graphics.Rect screenRect; + + /** + * Constructs a new PickPointFrustum from another Frustum and screen rectangle + * + * @param frustum frustum to create the PickPointFrustum from + * @param rect screen rectangle to store with this frustum + */ + public PickPointFrustum(Frustum frustum, android.graphics.Rect rect) + { + super(frustum.getLeft(), frustum.getRight(), frustum.getBottom(), frustum.getTop(), frustum.getNear(), + frustum.getFar()); + + if (rect == null) + { + String message = Logging.getMessage("nullValue.RectangleIsNull"); + Logging.verbose(message); + throw new IllegalArgumentException(message); + } + + this.screenRect = rect; + } + + // ============== Intersection Functions ======================= // + + /** + * Returns true if the specified 2D screen {@link android.graphics.Rect} intersects the space enclosed by this view + * aligned frustums screen rectangle. + * + * @param rect the rectangle to test + * + * @return true if the specified Rectangle intersects the space enclosed by this Frustum, and false otherwise. + * + * @throws IllegalArgumentException if the extent is null. + */ + public final boolean intersects(android.graphics.Rect rect) + { + if (rect == null) + { + String message = Logging.getMessage("nullValue.RectangleIsNull"); + Logging.verbose(message); + throw new IllegalArgumentException(message); + } + + return this.screenRect.intersect(rect); + } + + /** + * Returns true if the specified point is inside the 2D screen rectangle enclosed by this frustum + * + * @param x the x coordinate to test. + * @param y the y coordinate to test. + * + * @return true if the specified point is inside the space enclosed by this Frustum, and false otherwise. + * + * @throws IllegalArgumentException if the point is null. + */ + public final boolean contains(double x, double y) + { + return this.screenRect.contains((int) Math.ceil(x), (int) Math.ceil(y)); + } + + /** + * Returns true if the specified point is inside the 2D screen rectangle enclosed by this frustum + * + * @param point the point to test. + * + * @return true if the specified point is inside the space enclosed by this Frustum, and false otherwise. + * + * @throws IllegalArgumentException if the point is null. + */ + public final boolean contains(Point point) + { + if (point == null) + { + String msg = Logging.getMessage("nullValue.PointIsNull"); + Logging.verbose(msg); + throw new IllegalArgumentException(msg); + } + + return this.screenRect.contains(point.x, point.y); + } + + /** + * Returns a copy of this PickPointFrustum which is transformed by the specified Matrix. + * + * @param matrix the Matrix to transform this Frustum by. + * + * @return a copy of this Frustum, transformed by the specified Matrix. + * + * @throws IllegalArgumentException if the matrix is null + */ + public PickPointFrustum transformBy(Matrix matrix) + { + if (matrix == null) + { + String msg = Logging.getMessage("nullValue.MatrixIsNull"); + Logging.verbose(msg); + throw new IllegalArgumentException(msg); + } + + return new PickPointFrustum(super.transformBy(matrix), this.screenRect); + } + + /** + * Returns the screenRect associated with this frustum + * + * @return screenRect associated with this frustum + */ + public android.graphics.Rect getScreenRect() + { + return screenRect; + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/geom/Plane.java b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Plane.java index 0473405..68a4b32 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/geom/Plane.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Plane.java @@ -21,6 +21,36 @@ public class Plane // the plane normal and proportional distance. The vector is not necessarily a unit vector. protected final Vec4 vector; + /** + * Returns the plane that passes through the specified three points. The plane's normal is the cross product of the + * two vectors from pb to pa and pc to pa, respectively. The + * returned plane is undefined if any of the specified points are colinear. + * + * @param pa the first point. + * @param pb the second point. + * @param pc the third point. + * + * @return a Plane passing through the specified points. + * + * @throws IllegalArgumentException if pa, pb, or pc is null. + */ + public static Plane fromPoints(Vec4 pa, Vec4 pb, Vec4 pc) + { + if (pa == null || pb == null || pc == null) + { + String message = Logging.getMessage("nullValue.Vec4IsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + Vec4 vab = pb.subtract3(pa); + Vec4 vac = pc.subtract3(pa); + Vec4 n = vab.cross3(vac); + double d = -n.dot3(pa); + + return new Plane(n.x, n.y, n.z, d); + } + public Plane() { this.vector = new Vec4(); @@ -207,17 +237,201 @@ public double dot(Vec4 vec) return this.vector.x * vec.x + this.vector.y * vec.y + this.vector.z * vec.z + this.vector.w * vec.w; } - public double distanceTo(Vec4 point) - { - if (point == null) - { - String msg = Logging.getMessage("nullValue.PointIsNull"); - Logging.error(msg); - throw new IllegalArgumentException(msg); - } - - return this.vector.x * point.x + this.vector.y * point.y + this.vector.z * point.z + this.vector.w * point.w; - } + /** + * Clip a line segment to this plane. + * + * @param pa the first point of the segment. + * @param pb the second point of the segment. + * + * @return An array of two points both on the positive side of the plane. If the direction of the line formed by the + * two points is positive with respect to this plane's normal vector, the first point in the array will be + * the intersection point on the plane, and the second point will be the original segment end point. If the + * direction of the line is negative with respect to this plane's normal vector, the first point in the + * array will be the original segment's begin point, and the second point will be the intersection point on + * the plane. If the segment does not intersect the plane, null is returned. If the segment is coincident + * with the plane, the input points are returned, in their input order. + * + * @throws IllegalArgumentException if either input point is null. + */ + public Vec4[] clip(Vec4 pa, Vec4 pb) + { + if (pa == null || pb == null) + { + String message = Logging.getMessage("nullValue.PointIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (pa.equals(pb)) + return null; + + // Get the projection of the segment onto the plane. + Line line = Line.fromSegment(pa, pb); + double ldotv = this.vector.dot3(line.getDirection()); + + // Are the line and plane parallel? + if (ldotv == 0) // line and plane are parallel and maybe coincident + { + double ldots = this.vector.dot4(line.getOrigin()); + if (ldots == 0) + return new Vec4[] {pa, pb}; // line is coincident with the plane + else + return null; // line is not coincident with the plane + } + + // Not parallel so the line intersects. But does the segment intersect? + double t = -this.vector.dot4(line.getOrigin()) / ldotv; // ldots / ldotv + if (t < 0 || t > 1) // segment does not intersect + return null; + + Vec4 p = new Vec4(); + line.getPointAt(t, p); + if (ldotv > 0) + return new Vec4[] {p, pb}; + else + return new Vec4[] {pa, p}; + } + + public double distanceTo(Vec4 p) + { + return this.vector.dot4(p); + } + + /** + * Determines whether two points are on the same side of a plane. + * + * @param pa the first point. + * @param pb the second point. + * + * @return true if the points are on the same side of the plane, otherwise false. + * + * @throws IllegalArgumentException if either point is null. + */ + public int onSameSide(Vec4 pa, Vec4 pb) + { + if (pa == null || pb == null) + { + String message = Logging.getMessage("nullValue.PointIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + double da = this.distanceTo(pa); + double db = this.distanceTo(pb); + + if (da < 0 && db < 0) + return -1; + + if (da > 0 && db > 0) + return 1; + + return 0; + } + + /** + * Determines whether multiple points are on the same side of a plane. + * + * @param pts the array of points. + * + * @return true if the points are on the same side of the plane, otherwise false. + * + * @throws IllegalArgumentException if the points array is null or any point within it is null. + */ + public int onSameSide(Vec4[] pts) + { + if (pts == null) + { + String message = Logging.getMessage("nullValue.PointsArrayIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + double d = this.distanceTo(pts[0]); + int side = d < 0 ? -1 : d > 0 ? 1 : 0; + if (side == 0) + return 0; + + for (int i = 1; i < pts.length; i++) + { + if (pts[i] == null) + { + String message = Logging.getMessage("nullValue.PointIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + d = this.distanceTo(pts[i]); + if ((side == -1 && d < 0) || (side == 1 && d > 0)) + continue; + + return 0; // point is not on same side as the others + } + + return side; + } + + /** + * Determine the intersection point of a line with this plane. + * + * @param line the line to intersect. + * + * @return the intersection point if the line intersects the plane, otherwise null. + * + * @throws IllegalArgumentException if the line is null. + */ + public Vec4 intersect(Line line) + { + if (line == null) + { + String message = Logging.getMessage("nullValue.LineIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + double t = this.intersectDistance(line); + + if (Double.isNaN(t)) + return null; + + if (Double.isInfinite(t)) + return line.getOrigin(); + + Vec4 p = new Vec4(); + return line.getPointAt(t, p); + } + + /** + * Determine the parametric point of intersection of a line with this plane. + * + * @param line the line to test + * + * @return The parametric value of the point on the line at which it intersects the plane. {@link Double#NaN} is + * returned if the line does not intersect the plane. {@link Double#POSITIVE_INFINITY} is returned if the + * line is coincident with the plane. + * + * @throws IllegalArgumentException if the line is null. + */ + public double intersectDistance(Line line) + { + if (line == null) + { + String message = Logging.getMessage("nullValue.LineIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + double ldotv = this.vector.dot3(line.getDirection()); + if (ldotv == 0) // are line and plane parallel + { + double ldots = this.vector.dot4(line.getOrigin()); + if (ldots == 0) + return Double.POSITIVE_INFINITY; // line is coincident with the plane + else + return Double.NaN; // line is not coincident with the plane + } + + return -this.vector.dot4(line.getOrigin()) / ldotv; // ldots / ldotv + } @Override public boolean equals(Object o) diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/geom/Position.java b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Position.java index 92a90fc..825ac30 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/geom/Position.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Position.java @@ -4,6 +4,7 @@ */ package gov.nasa.worldwind.geom; +import android.os.Parcel; import gov.nasa.worldwind.util.Logging; import java.util.*; @@ -280,6 +281,14 @@ public Position add(Position that) return Position.fromDegrees(lat, lon, this.elevation + that.elevation); } + public Position subtract(Position that) + { + Angle lat = Angle.normalizedLatitude(this.latitude.subtract(that.latitude)); + Angle lon = Angle.normalizedLongitude(this.longitude.subtract(that.longitude)); + + return new Position(lat, lon, this.elevation - that.elevation); + } + @Override public boolean equals(Object o) { @@ -311,8 +320,34 @@ public String toString() sb.append("("); sb.append(this.latitude.toString()).append(", "); sb.append(this.longitude.toString()).append(", "); - sb.append(this.elevation); + sb.append(this.elevation+"m"); sb.append(")"); return sb.toString(); } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(this.latitude, flags); + dest.writeParcelable(this.longitude, flags); + dest.writeDouble(this.elevation); + } + + private static final ClassLoader LOADER = Position.class.getClassLoader(); + + public static final Creator CREATOR = new Creator() { + @Override + public Position createFromParcel(Parcel in) { + return new Position(in.readParcelable(LOADER), in.readParcelable(LOADER), in.readDouble()); + } + + @Override + public Position[] newArray(int size) { + return new Position[size]; + } + }; } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/geom/Quaternion.java b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Quaternion.java new file mode 100644 index 0000000..9ea39e3 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Quaternion.java @@ -0,0 +1,759 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.geom; + +import gov.nasa.worldwind.util.Logging; + +/** + * @author Chris Maxwell + * @version $Id$ + */ +public class Quaternion +{ + // Multiplicative identity quaternion. + public static final Quaternion IDENTITY = new Quaternion(0, 0, 0, 1); + + public final double x; + public final double y; + public final double z; + public final double w; + + // 4 values in a quaternion. + private static final int NUM_ELEMENTS = 4; + // Cached computations. + private int hashCode; + + public Quaternion(double x, double y, double z, double w) + { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + + public final boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + + Quaternion that = (Quaternion) obj; + return (this.x == that.x) + && (this.y == that.y) + && (this.z == that.z) + && (this.w == that.w); + } + + public final int hashCode() + { + if (this.hashCode == 0) + { + int result; + long tmp; + tmp = Double.doubleToLongBits(this.x); + result = (int) (tmp ^ (tmp >>> 32)); + tmp = Double.doubleToLongBits(this.y); + result = 31 * result + (int) (tmp ^ (tmp >>> 32)); + tmp = Double.doubleToLongBits(this.z); + result = 31 * result + (int) (tmp ^ (tmp >>> 32)); + tmp = Double.doubleToLongBits(this.w); + result = 31 * result + (int) (tmp ^ (tmp >>> 32)); + this.hashCode = result; + } + return this.hashCode; + } + + public static Quaternion fromArray(double[] compArray, int offset) + { + if (compArray == null) + { + String msg = Logging.getMessage("nullValue.ArrayIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + if ((compArray.length - offset) < NUM_ELEMENTS) + { + String msg = Logging.getMessage("generic.ArrayInvalidLength", compArray.length); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + //noinspection PointlessArithmeticExpression + return new Quaternion( + compArray[0 + offset], + compArray[1 + offset], + compArray[2 + offset], + compArray[3 + offset]); + } + + public final double[] toArray(double[] compArray, int offset) + { + if (compArray == null) + { + String msg = Logging.getMessage("nullValue.ArrayIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + if ((compArray.length - offset) < NUM_ELEMENTS) + { + String msg = Logging.getMessage("generic.ArrayInvalidLength", compArray.length); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + //noinspection PointlessArithmeticExpression + compArray[0 + offset] = this.x; + compArray[1 + offset] = this.y; + compArray[2 + offset] = this.z; + compArray[3 + offset] = this.w; + return compArray; + } + + public final String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("("); + sb.append(this.x).append(", "); + sb.append(this.y).append(", "); + sb.append(this.z).append(", "); + sb.append(this.w); + sb.append(")"); + return sb.toString(); + } + + public final double getX() + { + return this.x; + } + + public final double getY() + { + return this.y; + } + + public final double getZ() + { + return this.z; + } + + public final double getW() + { + return this.w; + } + + public final double x() + { + return this.x; + } + + public final double y() + { + return this.y; + } + + public final double z() + { + return this.z; + } + + public final double w() + { + return this.w; + } + + // ============== Factory Functions ======================= // + // ============== Factory Functions ======================= // + // ============== Factory Functions ======================= // + + public static Quaternion fromAxisAngle(Angle angle, Vec4 axis) + { + if (angle == null) + { + String msg = Logging.getMessage("nullValue.AngleIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + if (axis == null) + { + String msg = Logging.getMessage("nullValue.Vec4IsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + return fromAxisAngle(angle, axis.x, axis.y, axis.z, true); + } + + public static Quaternion fromAxisAngle(Angle angle, double axisX, double axisY, double axisZ) + { + if (angle == null) + { + String msg = Logging.getMessage("nullValue.AngleIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + return fromAxisAngle(angle, axisX, axisY, axisZ, true); + } + + private static Quaternion fromAxisAngle(Angle angle, double axisX, double axisY, double axisZ, boolean normalize) + { + if (angle == null) + { + String msg = Logging.getMessage("nullValue.AngleIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + if (normalize) + { + double length = Math.sqrt((axisX * axisX) + (axisY * axisY) + (axisZ * axisZ)); + if (!isZero(length) && (length != 1.0)) + { + axisX /= length; + axisY /= length; + axisZ /= length; + } + } + + double s = angle.sinHalfAngle(); + double c = angle.cosHalfAngle(); + return new Quaternion(axisX * s, axisY * s, axisZ * s, c); + } + + public static Quaternion fromMatrix(Matrix matrix) + { + if (matrix == null) + { + String msg = Logging.getMessage("nullValue.MatrixIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + double m11 = matrix.m[0]; + double m12 = matrix.m[1]; + double m13 = matrix.m[2]; + double m21 = matrix.m[4]; + double m22 = matrix.m[5]; + double m23 = matrix.m[6]; + double m31 = matrix.m[8]; + double m32 = matrix.m[9]; + double m33 = matrix.m[10]; + + double t = 1.0 + m11 + m22 + m33; + double x, y, z, w; + double s; + final double EPSILON = 0.00000001; + if (t > EPSILON) + { + s = 2.0 * Math.sqrt(t); + x = (m32 - m23) / s; + y = (m13 - m31) / s; + z = (m21 - m12) / s; + w = s / 4.0; + } + else if ((m11 > m22) && (m11 > m33)) + { + s = 2.0 * Math.sqrt(1.0 + m11 - m22 - m33); + x = s / 4.0; + y = (m21 + m12) / s; + z = (m13 + m31) / s; + w = (m32 - m23) / s; + } + else if (m22 > m33) + { + s = 2.0 * Math.sqrt(1.0 + m22 - m11 - m33); + x = (m21 + m12) / s; + y = s / 4.0; + z = (m32 + m23) / s; + w = (m13 - m31) / s; + } + else + { + s = 2.0 * Math.sqrt(1.0 + m33 - m11 - m22); + x = (m13 + m31) / s; + y = (m32 + m23) / s; + z = s / 4.0; + w = (m21 - m12) / s; + } + return new Quaternion(x, y, z, w); + } + + /** + * Returns a Quaternion created from three Euler angle rotations. The angles represent rotation about their + * respective unit-axes. The angles are applied in the order X, Y, Z. + * Angles can be extracted by calling {@link #getRotationX}, {@link #getRotationY}, {@link #getRotationZ}. + * + * @param x Angle rotation about unit-X axis. + * @param y Angle rotation about unit-Y axis. + * @param z Angle rotation about unit-Z axis. + * @return Quaternion representation of the combined X-Y-Z rotation. + */ + public static Quaternion fromRotationXYZ(Angle x, Angle y, Angle z) + { + if (x == null || y == null || z == null) + { + String msg = Logging.getMessage("nullValue.AngleIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + double cx = x.cosHalfAngle(); + double cy = y.cosHalfAngle(); + double cz = z.cosHalfAngle(); + double sx = x.sinHalfAngle(); + double sy = y.sinHalfAngle(); + double sz = z.sinHalfAngle(); + + // The order in which the three Euler angles are applied is critical. This can be thought of as multiplying + // three quaternions together, one for each Euler angle (and corresponding unit axis). Like matrices, + // quaternions affect vectors in reverse order. For example, suppose we construct a quaternion + // Q = (QX * QX) * QZ + // then transform some vector V by Q. This can be thought of as first transforming V by QZ, then QY, and + // finally by QX. This means that the order of quaternion multiplication is the reverse of the order in which + // the Euler angles are applied. + // + // The ordering below refers to the order in which angles are applied. + // + // QX = (sx, 0, 0, cx) + // QY = (0, sy, 0, cy) + // QZ = (0, 0, sz, cz) + // + // 1. XYZ Ordering + // (QZ * QY * QX) + // qw = (cx * cy * cz) + (sx * sy * sz); + // qx = (sx * cy * cz) - (cx * sy * sz); + // qy = (cx * sy * cz) + (sx * cy * sz); + // qz = (cx * cy * sz) - (sx * sy * cz); + // + // 2. ZYX Ordering + // (QX * QY * QZ) + // qw = (cx * cy * cz) - (sx * sy * sz); + // qx = (sx * cy * cz) + (cx * sy * sz); + // qy = (cx * sy * cz) - (sx * cy * sz); + // qz = (cx * cy * sz) + (sx * sy * cz); + // + + double qw = (cx * cy * cz) + (sx * sy * sz); + double qx = (sx * cy * cz) - (cx * sy * sz); + double qy = (cx * sy * cz) + (sx * cy * sz); + double qz = (cx * cy * sz) - (sx * sy * cz); + + return new Quaternion(qx, qy, qz, qw); + } + + /** + * Returns a Quaternion created from latitude and longitude rotations. + * Latitude and longitude can be extracted from a Quaternion by calling + * {@link #getLatLon}. + * + * @param latitude Angle rotation of latitude. + * @param longitude Angle rotation of longitude. + * @return Quaternion representing combined latitude and longitude rotation. + */ + public static Quaternion fromLatLon(Angle latitude, Angle longitude) + { + if (latitude == null || longitude == null) + { + String msg = Logging.getMessage("nullValue.AngleIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + double clat = latitude.cosHalfAngle(); + double clon = longitude.cosHalfAngle(); + double slat = latitude.sinHalfAngle(); + double slon = longitude.sinHalfAngle(); + + // The order in which the lat/lon angles are applied is critical. This can be thought of as multiplying two + // quaternions together, one for each lat/lon angle. Like matrices, quaternions affect vectors in reverse + // order. For example, suppose we construct a quaternion + // Q = QLat * QLon + // then transform some vector V by Q. This can be thought of as first transforming V by QLat, then QLon. This + // means that the order of quaternion multiplication is the reverse of the order in which the lat/lon angles + // are applied. + // + // The ordering below refers to order in which angles are applied. + // + // QLat = (0, slat, 0, clat) + // QLon = (slon, 0, 0, clon) + // + // 1. LatLon Ordering + // (QLon * QLat) + // qw = clat * clon; + // qx = clat * slon; + // qy = slat * clon; + // qz = slat * slon; + // + // 2. LonLat Ordering + // (QLat * QLon) + // qw = clat * clon; + // qx = clat * slon; + // qy = slat * clon; + // qz = - slat * slon; + // + + double qw = clat * clon; + double qx = clat * slon; + double qy = slat * clon; + double qz = 0.0 - slat * slon; + + return new Quaternion(qx, qy, qz, qw); + } + + // ============== Arithmetic Functions ======================= // + // ============== Arithmetic Functions ======================= // + // ============== Arithmetic Functions ======================= // + + public final Quaternion add(Quaternion quaternion) + { + if (quaternion == null) + { + String msg = Logging.getMessage("nullValue.QuaternionIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + return new Quaternion( + this.x + quaternion.x, + this.y + quaternion.y, + this.z + quaternion.z, + this.w + quaternion.w); + } + + public final Quaternion subtract(Quaternion quaternion) + { + if (quaternion == null) + { + String msg = Logging.getMessage("nullValue.QuaternionIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + return new Quaternion( + this.x - quaternion.x, + this.y - quaternion.y, + this.z - quaternion.z, + this.w - quaternion.w); + } + + public final Quaternion multiplyComponents(double value) + { + return new Quaternion( + this.x * value, + this.y * value, + this.z * value, + this.w * value); + } + + public final Quaternion multiply(Quaternion quaternion) + { + if (quaternion == null) + { + String msg = Logging.getMessage("nullValue.QuaternionIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + return new Quaternion( + (this.w * quaternion.x) + (this.x * quaternion.w) + (this.y * quaternion.z) - (this.z * quaternion.y), + (this.w * quaternion.y) + (this.y * quaternion.w) + (this.z * quaternion.x) - (this.x * quaternion.z), + (this.w * quaternion.z) + (this.z * quaternion.w) + (this.x * quaternion.y) - (this.y * quaternion.x), + (this.w * quaternion.w) - (this.x * quaternion.x) - (this.y * quaternion.y) - (this.z * quaternion.z)); + } + + public final Quaternion divideComponents(double value) + { + if (isZero(value)) + { + String msg = Logging.getMessage("generic.ArgumentOutOfRange", value); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + return new Quaternion( + this.x / value, + this.y / value, + this.z / value, + this.w / value); + } + + public final Quaternion divideComponents(Quaternion quaternion) + { + if (quaternion == null) + { + String msg = Logging.getMessage("nullValue.QuaternionIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + return new Quaternion( + this.x / quaternion.x, + this.y / quaternion.y, + this.z / quaternion.z, + this.w / quaternion.w); + } + + public final Quaternion getConjugate() + { + return new Quaternion( + 0.0 - this.x, + 0.0 - this.y, + 0.0 - this.z, + this.w); + } + + public final Quaternion getNegative() + { + return new Quaternion( + 0.0 - this.x, + 0.0 - this.y, + 0.0 - this.z, + 0.0 - this.w); + } + + // ============== Geometric Functions ======================= // + // ============== Geometric Functions ======================= // + // ============== Geometric Functions ======================= // + + public final double getLength() + { + return Math.sqrt(this.getLengthSquared()); + } + + public final double getLengthSquared() + { + return (this.x * this.x) + + (this.y * this.y) + + (this.z * this.z) + + (this.w * this.w); + } + + public final Quaternion normalize() + { + double length = this.getLength(); + // Vector has zero length. + if (isZero(length)) + { + return this; + } + else + { + return new Quaternion( + this.x / length, + this.y / length, + this.z / length, + this.w / length); + } + } + + public final double dot(Quaternion quaternion) + { + if (quaternion == null) + { + String msg = Logging.getMessage("nullValue.QuaternionIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + return (this.x * quaternion.x) + (this.y * quaternion.y) + (this.z * quaternion.z) + (this.w * quaternion.w); + } + + public final Quaternion getInverse() + { + double length = this.getLength(); + // Vector has zero length. + if (isZero(length)) + { + return this; + } + else + { + return new Quaternion( + (0.0 - this.x) / length, + (0.0 - this.y) / length, + (0.0 - this.z) / length, + this.w / length); + } + } + + // ============== Mixing Functions ======================= // + // ============== Mixing Functions ======================= // + // ============== Mixing Functions ======================= // + + public static Quaternion mix(double amount, Quaternion value1, Quaternion value2) + { + if ((value1 == null) || (value2 == null)) + { + String msg = Logging.getMessage("nullValue.QuaternionIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + if (amount < 0.0) + return value1; + else if (amount > 1.0) + return value2; + + double t1 = 1.0 - amount; + return new Quaternion( + (value1.x * t1) + (value2.x * amount), + (value1.y * t1) + (value2.y * amount), + (value1.z * t1) + (value2.z * amount), + (value1.w * t1) + (value2.w * amount)); + } + + public static Quaternion slerp(double amount, Quaternion value1, Quaternion value2) + { + if ((value1 == null) || (value2 == null)) + { + String msg = Logging.getMessage("nullValue.QuaternionIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + if (amount < 0.0) + return value1; + else if (amount > 1.0) + return value2; + + double dot = value1.dot(value2); + double x2, y2, z2, w2; + if (dot < 0.0) + { + dot = 0.0 - dot; + x2 = 0.0 - value2.x; + y2 = 0.0 - value2.y; + z2 = 0.0 - value2.z; + w2 = 0.0 - value2.w; + } + else + { + x2 = value2.x; + y2 = value2.y; + z2 = value2.z; + w2 = value2.w; + } + + double t1, t2; + + final double EPSILON = 0.0001; + if ((1.0 - dot) > EPSILON) // standard case (slerp) + { + double angle = Math.acos(dot); + double sinAngle = Math.sin(angle); + t1 = Math.sin((1.0 - amount) * angle) / sinAngle; + t2 = Math.sin(amount * angle) / sinAngle; + } + else // just lerp + { + t1 = 1.0 - amount; + t2 = amount; + } + + return new Quaternion( + (value1.x * t1) + (x2 * t2), + (value1.y * t1) + (y2 * t2), + (value1.z * t1) + (z2 * t2), + (value1.w * t1) + (w2 * t2)); + } + + // ============== Accessor Functions ======================= // + // ============== Accessor Functions ======================= // + // ============== Accessor Functions ======================= // + + public final Angle getAngle() + { + double w = this.w; + + double length = this.getLength(); + if (!isZero(length) && (length != 1.0)) + w /= length; + + double radians = 2.0 * Math.acos(w); + if (Double.isNaN(radians)) + return null; + + return Angle.fromRadians(radians); + } + + public final Vec4 getAxis() + { + double x = this.x; + double y = this.y; + double z = this.z; + + double length = this.getLength(); + if (!isZero(length) && (length != 1.0)) + { + x /= length; + y /= length; + z /= length; + } + + double vecLength = Math.sqrt((x * x) + (y * y) + (z * z)); + if (!isZero(vecLength) && (vecLength != 1.0)) + { + x /= vecLength; + y /= vecLength; + z /= vecLength; + } + + return new Vec4(x, y, z); + } + + public final Angle getRotationX() + { + double radians = Math.atan2((2.0 * this.x * this.w) - (2.0 * this.y * this.z), + 1.0 - 2.0 * (this.x * this.x) - 2.0 * (this.z * this.z)); + if (Double.isNaN(radians)) + return null; + + return Angle.fromRadians(radians); + } + + public final Angle getRotationY() + { + double radians = Math.atan2((2.0 * this.y * this.w) - (2.0 * this.x * this.z), + 1.0 - (2.0 * this.y * this.y) - (2.0 * this.z * this.z)); + if (Double.isNaN(radians)) + return null; + + return Angle.fromRadians(radians); + } + + public final Angle getRotationZ() + { + double radians = Math.asin((2.0 * this.x * this.y) + (2.0 * this.z * this.w)); + if (Double.isNaN(radians)) + return null; + + return Angle.fromRadians(radians); + } + + public final LatLon getLatLon() + { + double latRadians = Math.asin((2.0 * this.y * this.w) - (2.0 * this.x * this.z)); + double lonRadians = Math.atan2((2.0 * this.y * this.z) + (2.0 * this.x * this.w), + (this.w * this.w) - (this.x * this.x) - (this.y * this.y) + (this.z * this.z)); + if (Double.isNaN(latRadians) || Double.isNaN(lonRadians)) + return null; + + return LatLon.fromRadians(latRadians, lonRadians); + } + + // ============== Helper Functions ======================= // + // ============== Helper Functions ======================= // + // ============== Helper Functions ======================= // + + private static final Double PositiveZero = +0.0d; + + private static final Double NegativeZero = -0.0d; + + private static boolean isZero(double value) + { + return (PositiveZero.compareTo(value) == 0) + || (NegativeZero.compareTo(value) == 0); + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/geom/Sector.java b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Sector.java index fc7d4b3..34547d6 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/geom/Sector.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Sector.java @@ -6,6 +6,7 @@ package gov.nasa.worldwind.geom; import gov.nasa.worldwind.globes.Globe; +import gov.nasa.worldwind.render.DrawContext; import gov.nasa.worldwind.util.Logging; import java.util.ArrayList; import java.util.Iterator; @@ -17,11 +18,277 @@ * @version $Id: Sector.java 811 2012-09-26 17:44:35Z dcollins $ */ public class Sector implements Iterable { + /** A Sector of latitude [-90 degrees, + 90 degrees] and longitude [-180 degrees, + 180 degrees]. */ + public static final Sector FULL_SPHERE = new Sector(Angle.NEG90, Angle.POS90, Angle.NEG180, Angle.POS180); + public static final Sector EMPTY_SECTOR = new Sector(Angle.ZERO, Angle.ZERO, Angle.ZERO, Angle.ZERO); + public final Angle minLatitude; public final Angle maxLatitude; public final Angle minLongitude; public final Angle maxLongitude; + public static Sector boundingSector(LatLon pA, LatLon pB) + { + if (pA == null || pB == null) + { + String message = Logging.getMessage("nullValue.PositionsListIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + double minLat = pA.getLatitude().degrees; + double minLon = pA.getLongitude().degrees; + double maxLat = pA.getLatitude().degrees; + double maxLon = pA.getLongitude().degrees; + + if (pB.getLatitude().degrees < minLat) + minLat = pB.getLatitude().degrees; + else if (pB.getLatitude().degrees > maxLat) + maxLat = pB.getLatitude().degrees; + + if (pB.getLongitude().degrees < minLon) + minLon = pB.getLongitude().degrees; + else if (pB.getLongitude().degrees > maxLon) + maxLon = pB.getLongitude().degrees; + + return Sector.fromDegrees(minLat, maxLat, minLon, maxLon); + } + + /** + * Returns a new Sector encompassing a circle centered at a given position, with a given radius in + * meter. + * + * @param globe a Globe instance. + * @param center the circle center position. + * @param radius the circle radius in meter. + * + * @return the circle bounding sector. + */ + public static Sector boundingSector(Globe globe, LatLon center, double radius) + { + double halfDeltaLatRadians = radius / globe.getRadiusAt(center); + double halfDeltaLonRadians = Math.PI * 2; + if (center.getLatitude().cos() > 0) + halfDeltaLonRadians = halfDeltaLatRadians / center.getLatitude().cos(); + + return new Sector( + Angle.fromRadiansLatitude(center.getLatitude().radians - halfDeltaLatRadians), + Angle.fromRadiansLatitude(center.getLatitude().radians + halfDeltaLatRadians), + Angle.fromRadiansLongitude(center.getLongitude().radians - halfDeltaLonRadians), + Angle.fromRadiansLongitude(center.getLongitude().radians + halfDeltaLonRadians)); + } + + public static Sector boundingSector(Iterable locations) + { + if (locations == null) + { + String message = Logging.getMessage("nullValue.PositionsListIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (!locations.iterator().hasNext()) + return EMPTY_SECTOR; // TODO: should be returning null + + double minLat = Angle.POS90.getDegrees(); + double minLon = Angle.POS180.getDegrees(); + double maxLat = Angle.NEG90.getDegrees(); + double maxLon = Angle.NEG180.getDegrees(); + + for (LatLon p : locations) + { + double lat = p.getLatitude().getDegrees(); + if (lat < minLat) + minLat = lat; + if (lat > maxLat) + maxLat = lat; + + double lon = p.getLongitude().getDegrees(); + if (lon < minLon) + minLon = lon; + if (lon > maxLon) + maxLon = lon; + } + + return Sector.fromDegrees(minLat, maxLat, minLon, maxLon); + } + + /** + * Returns an array of Sectors encompassing a circle centered at a given position, with a given radius in meters. If + * the geometry defined by the circle and radius spans the international dateline, this will return two sectors, one + * for each side of the dateline. Otherwise, this will return a single bounding sector. This returns null if the + * radius is zero. + * + * @param globe a Globe instance. + * @param center the circle center location. + * @param radius the circle radius in meters. + * + * @return the circle's bounding sectors, or null if the radius is zero. + * + * @throws IllegalArgumentException if either the globe or center is null, or if the radius is less than zero. + */ + public static Sector[] splitBoundingSectors(Globe globe, LatLon center, double radius) + { + if (globe == null) + { + String message = Logging.getMessage("nullValue.GlobeIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (center == null) + { + String message = Logging.getMessage("nullValue.CenterIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (radius < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "radius < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + double halfDeltaLatRadians = radius / globe.getRadiusAt(center); + + double minLat = center.getLatitude().radians - halfDeltaLatRadians; + double maxLat = center.getLatitude().radians + halfDeltaLatRadians; + + double minLon; + double maxLon; + + // If the circle does not cross a pole, then compute the max and min longitude. + if (minLat >= Angle.NEG90.radians && maxLat <= Angle.POS90.radians) + { + // We want to find the maximum and minimum longitude values on the circle. We will start with equation 5-6 + // from "Map Projections - A Working Manual", page 31, and solve for the value of Az that will maximize + // lon - lon0. + // + // Eq. 5-6: + // lon = lon0 + arctan( h(lat0, c, az) ) + // h(lat0, c, az) = sin(c) sin(az) / (cos(lat0) cos(c) - sin(lat1) sin(c) cos(Az)) + // + // Where (lat0, lon0) are the starting coordinates, c is the angular distance along the great circle from + // the starting coordinate, Az is the azimuth, and lon is the end position longitude. All values are in + // radians. + // + // lon - lon0 is maximized when h(lat0, c, Az) is maximized because arctan(x) -> 1 as x -> infinity. + // + // Taking the partial derivative of h with respect to Az we get: + // h'(Az) = (sin(c) cos(c) cos(lat0) cos(Az) - sin^2(c) sin(lat0)) / (cos(lat0) cos(c) - sin(lat0) sin(c) cos(Az))^2 + // + // Setting h' = 0 to find maxima: + // 0 = sin(c) cos(c) cos(lat0) cos(Az) - sin^2(c) sin(lat0) + // + // And solving for Az: + // Az = arccos( tan(lat0) tan(c) ) + // + // +/- Az are bearings from North that will give positions of max and min longitude. + + // If halfDeltaLatRadians == 90 degrees, then tan is undefined. This can happen if the circle radius is one + // quarter of the globe diameter, and the circle is centered on the equator. tan(center lat) is always + // defined because the center lat is in the range -90 to 90 exclusive. If it were equal to 90, then the + // circle would cover a pole. + double az; + if (Math.abs(Angle.POS90.radians - halfDeltaLatRadians) + > 0.001) // Consider within 1/1000th of a radian to be equal + az = Math.acos(Math.tan(halfDeltaLatRadians) * Math.tan(center.latitude.radians)); + else + az = Angle.POS90.radians; + + LatLon east = LatLon.greatCircleEndPosition(center, az, halfDeltaLatRadians); + LatLon west = LatLon.greatCircleEndPosition(center, -az, halfDeltaLatRadians); + + minLon = Math.min(east.longitude.radians, west.longitude.radians); + maxLon = Math.max(east.longitude.radians, west.longitude.radians); + } + else + { + // If the circle crosses the pole then it spans the full circle of longitude + minLon = Angle.NEG180.radians; + maxLon = Angle.POS180.radians; + } + + LatLon ll = new LatLon( + Angle.fromRadiansLatitude(minLat), + Angle.normalizedLongitude(Angle.fromRadians(minLon))); + LatLon ur = new LatLon( + Angle.fromRadiansLatitude(maxLat), + Angle.normalizedLongitude(Angle.fromRadians(maxLon))); + + Iterable locations = java.util.Arrays.asList(ll, ur); + + if (LatLon.locationsCrossDateLine(locations)) + { + return splitBoundingSectors(locations); + } + else + { + Sector s = boundingSector(locations); + return (s != null && !s.equals(Sector.EMPTY_SECTOR)) ? new Sector[] {s} : null; + } + } + + public static Sector[] splitBoundingSectors(Iterable locations) + { + if (locations == null) + { + String message = Logging.getMessage("nullValue.LocationInListIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (!locations.iterator().hasNext()) + return null; + + double minLat = Angle.POS90.getDegrees(); + double minLon = Angle.POS180.getDegrees(); + double maxLat = Angle.NEG90.getDegrees(); + double maxLon = Angle.NEG180.getDegrees(); + + LatLon lastLocation = null; + + for (LatLon ll : locations) + { + double lat = ll.getLatitude().getDegrees(); + if (lat < minLat) + minLat = lat; + if (lat > maxLat) + maxLat = lat; + + double lon = ll.getLongitude().getDegrees(); + if (lon >= 0 && lon < minLon) + minLon = lon; + if (lon <= 0 && lon > maxLon) + maxLon = lon; + + if (lastLocation != null) + { + double lastLon = lastLocation.getLongitude().getDegrees(); + if (Math.signum(lon) != Math.signum(lastLon)) + { + if (Math.abs(lon - lastLon) < 180) + { + // Crossing the zero longitude line too + maxLon = 0; + minLon = 0; + } + } + } + lastLocation = ll; + } + + if (minLat == maxLat && minLon == maxLon) + return null; + + return new Sector[] + { + Sector.fromDegrees(minLat, maxLat, minLon, 180), // Sector on eastern hemisphere. + Sector.fromDegrees(minLat, maxLat, -180, maxLon) // Sector on western hemisphere. + }; + } + public Sector() { this.minLatitude = new Angle(); this.maxLatitude = new Angle(); @@ -480,6 +747,63 @@ public Sector[] subdivide(int div) { return sectors; } + /** + * Returns an approximation of the distance in model coordinates between the surface geometry defined by this sector + * and the specified model coordinate point. The returned value represents the shortest distance between the + * specified point and this sector's corner points or its center point. The draw context defines the globe and the + * elevations that are used to compute the corner points and the center point. + * + * @param dc The draw context defining the surface geometry. + * @param point The model coordinate point to compute a distance to. + * + * @return The distance between this sector's surface geometry and the specified point, in model coordinates. + * + * @throws IllegalArgumentException if any argument is null. + */ + public double distanceTo(DrawContext dc, Vec4 point) + { + if (dc == null) + { + String message = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (point == null) + { + String message = Logging.getMessage("nullValue.PointIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + Vec4[] corners = new Vec4[4]; + for(int i=0; i d2) + minDistance = d2; + if (minDistance > d3) + minDistance = d3; + if (minDistance > d4) + minDistance = d4; + if (minDistance > d5) + minDistance = d5; + + return minDistance; + } + /** * Computes the Cartesian coordinates of a Sector's center. * @@ -505,8 +829,8 @@ public void computeCentroidPoint(Globe globe, double exaggeration, Vec4 result) throw new IllegalArgumentException(msg); } - Angle lat = Angle.fromDegrees(0.5 * (this.minLatitude.radians + this.maxLatitude.radians)); - Angle lon = Angle.fromDegrees(0.5 * (this.minLongitude.radians + this.maxLongitude.radians)); + Angle lat = Angle.fromDegrees(0.5 * (this.minLatitude.degrees + this.maxLatitude.degrees)); + Angle lon = Angle.fromDegrees(0.5 * (this.minLongitude.degrees + this.maxLongitude.degrees)); globe.computePointFromPosition(lat, lon, exaggeration * globe.getElevation(lat, lon), result); } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/geom/Vec4.java b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Vec4.java index 4be0bff..60838d9 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/geom/Vec4.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/geom/Vec4.java @@ -15,6 +15,17 @@ */ public class Vec4 { + public static final Vec4 ZERO = new Vec4(0, 0, 0, 1); + public static final Vec4 ONE = new Vec4(1, 1, 1, 1); + public static final Vec4 UNIT_X = new Vec4(1, 0, 0, 0); + public static final Vec4 UNIT_NEGATIVE_X = new Vec4(-1, 0, 0, 0); + public static final Vec4 UNIT_Y = new Vec4(0, 1, 0, 0); + public static final Vec4 UNIT_NEGATIVE_Y = new Vec4(0, -1, 0, 0); + public static final Vec4 UNIT_Z = new Vec4(0, 0, 1, 0); + public static final Vec4 UNIT_NEGATIVE_Z = new Vec4(0, 0, -1, 0); + public static final Vec4 UNIT_W = new Vec4(0, 0, 0, 1); + public static final Vec4 UNIT_NEGATIVE_W = new Vec4(0, 0, 0, -1); + public double x; public double y; public double z; @@ -58,6 +69,76 @@ public static double getLengthSquared3(double x, double y, double z) return x * x + y * y + z * z; } + // ============== Mixing Functions ======================= // + // ============== Mixing Functions ======================= // + // ============== Mixing Functions ======================= // + + public static Vec4 min3(Vec4 value1, Vec4 value2) + { + if ((value1 == null) || (value2 == null)) + { + String msg = Logging.getMessage("nullValue.Vec4IsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + return new Vec4( + (value1.x < value2.x) ? value1.x : value2.x, + (value1.y < value2.y) ? value1.y : value2.y, + (value1.z < value2.z) ? value1.z : value2.z); + } + + public static Vec4 max3(Vec4 value1, Vec4 value2) + { + if ((value1 == null) || (value2 == null)) + { + String msg = Logging.getMessage("nullValue.Vec4IsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + return new Vec4( + (value1.x > value2.x) ? value1.x : value2.x, + (value1.y > value2.y) ? value1.y : value2.y, + (value1.z > value2.z) ? value1.z : value2.z); + } + + public static Vec4 clamp3(Vec4 vec4, double min, double max) + { + if (vec4 == null) + { + String msg = Logging.getMessage("nullValue.Vec4IsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + return new Vec4( + (vec4.x < min) ? min : ((vec4.x > max) ? max : vec4.x), + (vec4.y < min) ? min : ((vec4.y > max) ? max : vec4.y), + (vec4.z < min) ? min : ((vec4.z > max) ? max : vec4.z)); + } + + public static Vec4 mix3(double amount, Vec4 value1, Vec4 value2) + { + if ((value1 == null) || (value2 == null)) + { + String msg = Logging.getMessage("nullValue.Vec4IsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + if (amount < 0.0) + return value1; + else if (amount > 1.0) + return value2; + + double t1 = 1.0 - amount; + return new Vec4( + (value1.x * t1) + (value2.x * amount), + (value1.y * t1) + (value2.y * amount), + (value1.z * t1) + (value2.z * amount)); + } + public Vec4 copy() { return new Vec4(this.x, this.y, this.z, this.w); @@ -999,4 +1080,30 @@ public void toArray3f(float[] array, int offset) array[offset + 1] = (float) this.y; array[offset + 2] = (float) this.z; } + + /** + * Indicates whether three vectors are colinear. + * + * @param a the first vector. + * @param b the second vector. + * @param c the third vector. + * + * @return true if the vectors are colinear, otherwise false. + * + * @throws IllegalArgumentException if any argument is null. + */ + public static boolean areColinear(Vec4 a, Vec4 b, Vec4 c) + { + if (a == null || b == null || c == null) + { + String msg = Logging.getMessage("nullValue.Vec4IsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + Vec4 ab = b.subtract3(a).normalize3(); + Vec4 bc = c.subtract3(b).normalize3(); + + return Math.abs(ab.dot3(bc)) > 0.999; // ab and bc are considered colinear if their dot product is near +/-1 + } } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/globes/EllipsoidalGlobe.java b/WorldWindAndroid/src/gov/nasa/worldwind/globes/EllipsoidalGlobe.java index d407862..cd09f04 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/globes/EllipsoidalGlobe.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/globes/EllipsoidalGlobe.java @@ -4,18 +4,13 @@ */ package gov.nasa.worldwind.globes; -import gov.nasa.worldwind.geom.Angle; -import gov.nasa.worldwind.geom.Intersection; -import gov.nasa.worldwind.geom.LatLon; -import gov.nasa.worldwind.geom.Line; -import gov.nasa.worldwind.geom.Matrix; -import gov.nasa.worldwind.geom.Position; -import gov.nasa.worldwind.geom.Sector; -import gov.nasa.worldwind.geom.Vec4; +import gov.nasa.worldwind.View; +import gov.nasa.worldwind.geom.*; import gov.nasa.worldwind.render.DrawContext; import gov.nasa.worldwind.terrain.ElevationModel; import gov.nasa.worldwind.terrain.Tessellator; import gov.nasa.worldwind.util.Logging; +import gov.nasa.worldwind.util.WWMath; /** * Edited By: Nicola Dorigatti, Trilogis @@ -43,6 +38,14 @@ public StateKey(DrawContext dc) { this.verticalExaggeration = dc.getVerticalExaggeration(); } + public StateKey(Globe globe) + { + this.globe = globe; + this.tessellator = EllipsoidalGlobe.this.tessellator; + this.verticalExaggeration = 1; + this.elevationModel = this.globe.getElevationModel(); + } + public Globe getGlobe() { return this.globe; } @@ -100,25 +103,123 @@ public EllipsoidalGlobe(double equatorialRadius, double polarRadius, double es, this.setElevationModel(elevationModel); } + public Object getStateKey(DrawContext dc) + { + return this.getGlobeStateKey(dc); + } + /** {@inheritDoc} */ public GlobeStateKey getGlobeStateKey(DrawContext dc) { return new StateKey(dc); } + public GlobeStateKey getGlobeStateKey() + { + return new StateKey(this); + } + /** {@inheritDoc} */ public double getRadius() { return this.equatorialRadius; } + public double getEquatorialRadius() + { + return this.equatorialRadius; + } + + public double getPolarRadius() + { + return this.polarRadius; + } + + public double getMaximumRadius() + { + return this.equatorialRadius; + } + + public double getRadiusAt(Angle latitude, Angle longitude) + { + if (latitude == null || longitude == null) + { + String msg = Logging.getMessage("nullValue.AngleIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + return this.computePointFromPosition(latitude, longitude, 0d).getLength3(); + } + + public double getRadiusAt(LatLon latLon) + { + if (latLon == null) + { + String msg = Logging.getMessage("nullValue.LatLonIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + return this.computePointFromPosition(latLon.getLatitude(), latLon.getLongitude(), 0d).getLength3(); + } + + public Vec4 getCenter() + { + return this.center; + } + + public double getEffectiveRadius(Plane plane) + { + return this.getRadius(); + } + + public boolean intersects(Frustum frustum) + { + if (frustum == null) + { + String message = Logging.getMessage("nullValue.FrustumIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } +// See if the extent's bounding sphere is within or intersects the frustum. The dot product of the extent's + // center point with each plane's vector provides a distance to each plane. If this distance is less than + // -radius, the extent is completely clipped by that plane and therefore does not intersect the space enclosed + // by this Frustum. + + Vec4 c = this.getCenter(); + double nr = -this.getRadius(); + + if (frustum.getFar().dot(c) <= nr) + return false; + if (frustum.getLeft().dot(c) <= nr) + return false; + if (frustum.getRight().dot(c) <= nr) + return false; + if (frustum.getTop().dot(c) <= nr) + return false; + if (frustum.getBottom().dot(c) <= nr) + return false; + //noinspection RedundantIfStatement + if (frustum.getNear().dot(c) <= nr) + return false; + + return true; + } + /** {@inheritDoc} */ public Intersection[] intersect(Line line) { + return this.intersect(line, 0); + } + + /** {@inheritDoc} */ + public Intersection[] intersect(Line line, double altitude) + { if (line == null) { String msg = Logging.getMessage("nullValue.LineIsNull"); Logging.error(msg); throw new IllegalArgumentException(msg); } - return this.intersect(line, this.equatorialRadius, this.polarRadius); + return this.intersect(line, this.equatorialRadius + altitude, this.polarRadius + altitude); } /** {@inheritDoc} */ @@ -189,6 +290,74 @@ protected static double discriminant(double a, double b, double c) { return b * b - 4 * a * c; } + public Intersection[] intersect(Triangle t, double elevation) + { + if (t == null) + return null; + + boolean bA = isPointAboveElevation(t.getA(), elevation); + boolean bB = isPointAboveElevation(t.getB(), elevation); + boolean bC = isPointAboveElevation(t.getC(), elevation); + + if (!(bA ^ bB) && !(bB ^ bC)) + return null; // all triangle points are either above or below the given elevation + + Intersection[] inter = new Intersection[2]; + int idx = 0; + + // Assumes that intersect(Line) returns only one intersection when the line + // originates inside the ellipsoid at the given elevation. + if (bA ^ bB) + if (bA) + inter[idx++] = intersect(new Line(t.getB(), t.getA().subtract3(t.getB())), elevation)[0]; + else + inter[idx++] = intersect(new Line(t.getA(), t.getB().subtract3(t.getA())), elevation)[0]; + + if (bB ^ bC) + if (bB) + inter[idx++] = intersect(new Line(t.getC(), t.getB().subtract3(t.getC())), elevation)[0]; + else + inter[idx++] = intersect(new Line(t.getB(), t.getC().subtract3(t.getB())), elevation)[0]; + + if (bC ^ bA) + if (bC) + inter[idx] = intersect(new Line(t.getA(), t.getC().subtract3(t.getA())), elevation)[0]; + else + inter[idx] = intersect(new Line(t.getC(), t.getA().subtract3(t.getC())), elevation)[0]; + + return inter; + } + + public boolean intersects(Line line) + { + if (line == null) + return false; + + return line.distanceTo(this.center) <= this.equatorialRadius; + } + + public boolean intersects(Plane plane) + { + if (plane == null) + return false; + + double dq1 = plane.dot(this.center); + return dq1 <= this.equatorialRadius; + } + + /** {@inheritDoc} */ + public double getProjectedArea(View view) + { + if (view == null) + { + String message = Logging.getMessage("nullValue.ViewIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + return WWMath.computeSphereProjectedArea(view, this.getCenter(), this.getRadius()); + } + /** {@inheritDoc} */ public Vec4 computePointFromPosition(Position position) { if (position == null) { @@ -833,4 +1002,15 @@ public Matrix computeViewOrientationAtPosition(Angle latitude, Angle longitude, return transform; } + + public boolean isPointAboveElevation(Vec4 point, double elevation) + { + if (point == null) + return false; + + return (point.x * point.x) / ((this.equatorialRadius + elevation) * (this.equatorialRadius + elevation)) + + (point.y * point.y) / ((this.polarRadius + elevation) * (this.polarRadius + elevation)) + + (point.z * point.z) / ((this.equatorialRadius + elevation) * (this.equatorialRadius + elevation)) + - 1 > 0; + } } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/globes/Globe.java b/WorldWindAndroid/src/gov/nasa/worldwind/globes/Globe.java index 35c4650..07e4e18 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/globes/Globe.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/globes/Globe.java @@ -5,14 +5,7 @@ package gov.nasa.worldwind.globes; import gov.nasa.worldwind.WWObject; -import gov.nasa.worldwind.geom.Angle; -import gov.nasa.worldwind.geom.Intersection; -import gov.nasa.worldwind.geom.LatLon; -import gov.nasa.worldwind.geom.Line; -import gov.nasa.worldwind.geom.Matrix; -import gov.nasa.worldwind.geom.Position; -import gov.nasa.worldwind.geom.Sector; -import gov.nasa.worldwind.geom.Vec4; +import gov.nasa.worldwind.geom.*; import gov.nasa.worldwind.render.DrawContext; import gov.nasa.worldwind.terrain.ElevationModel; import gov.nasa.worldwind.terrain.SectorGeometryList; @@ -24,7 +17,21 @@ * @author dcollins * @version $Id: Globe.java 827 2012-10-08 19:32:08Z tgaskins $ */ -public interface Globe extends WWObject { +public interface Globe extends WWObject, Extent { + + + /** + * Returns a state key identifying this globe's current configuration. Can be used to subsequently determine whether + * the globe's configuration has changed. + * + * @param dc the current draw context. + * + * @return a state key for the globe's current configuration. + * + * @throws IllegalArgumentException if the draw context is null. + */ + Object getStateKey(DrawContext dc); + /** * Returns a typed state key identifying this globe's current configuration. Can be used to subsequently determine * whether the globe's configuration has changed. @@ -37,6 +44,16 @@ public interface Globe extends WWObject { */ GlobeStateKey getGlobeStateKey(DrawContext dc); + /** + * Returns a typed state key identifying this globe's current configuration. Can be used to subsequently determine + * whether the globe's configuration has changed. + * + * @return a state key for the globe's current configuration. + * + * @throws IllegalArgumentException if the draw context is null. + */ + GlobeStateKey getGlobeStateKey(); + /** * Indicates this globe's elevation model. * @@ -78,9 +95,29 @@ public interface Globe extends WWObject { */ SectorGeometryList tessellate(DrawContext dc); - double getRadius(); + /** + * Intersects a specified line with this globe. Only the ellipsoid itself is considered; terrain elevations are not + * incorporated. + * + * @param line the line to intersect. + * @param altitude a distance in meters to expand the globe's equatorial and polar radii prior to performing the + * intersection. + * + * @return the intersection points, or null if no intersection occurs or the line is null. + */ + Intersection[] intersect(Line line, double altitude); - Intersection[] intersect(Line line); + /** + * Intersects a specified triangle with the globe. Only the ellipsoid itself is considered; terrain elevations are + * not incorporated. + * + * @param triangle the triangle to intersect. + * @param altitude a distance in meters to expand the globe's equatorial and polar radii prior to performing the + * intersection. + * + * @return the intersection points, or null if no intersection occurs or triangle is null. + */ + Intersection[] intersect(Triangle triangle, double altitude); boolean getIntersectionPosition(Line line, Position result); @@ -99,6 +136,46 @@ public interface Globe extends WWObject { double getMaxElevation(); + /** + * Indicates the radius of the globe at the equator, in meters. + * + * @return The radius at the equator, in meters. + */ + double getEquatorialRadius(); + + /** + * Indicates the radius of the globe at the poles, in meters. + * + * @return The radius at the poles, in meters. + */ + double getPolarRadius(); + + /** + * Indicates the maximum radius on the globe. + * + * @return The maximum radius, in meters. + */ + double getMaximumRadius(); + + /** + * Indicates the radius of the globe at a location. + * + * @param latitude Latitude of the location at which to determine radius. + * @param longitude Longitude of the location at which to determine radius. + * + * @return The radius of the globe at the specified location, in meters. + */ + double getRadiusAt(Angle latitude, Angle longitude); + + /** + * Indicates this globe's radius at a specified location. + * + * @param latLon the location of interest. + * + * @return the globe's radius at that location. + */ + double getRadiusAt(LatLon latLon); + /** * Returns the minimum and maximum elevations within a specified sector on this Globe. This returns a two-element * array filled with zero if this Globe has no elevation model. @@ -200,4 +277,14 @@ public interface Globe extends WWObject { void computeNorthPointingTangentAtLocation(Angle latitude, Angle longitude, Vec4 result); Matrix computeViewOrientationAtPosition(Angle latitude, Angle longitude, double metersElevation); + + /** + * Determines whether a point is above a given elevation. + * + * @param point the Vec4 point to test. If null, this method returns false. + * @param elevation the elevation to test for. + * + * @return true if the given point is above the given elevation, otherwise false. + */ + boolean isPointAboveElevation(Vec4 point, double elevation); } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/layers/AbstractLayer.java b/WorldWindAndroid/src/gov/nasa/worldwind/layers/AbstractLayer.java index 7798d38..95c2f5b 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/layers/AbstractLayer.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/layers/AbstractLayer.java @@ -146,7 +146,7 @@ public boolean isLayerActive(DrawContext dc) { throw new IllegalStateException(message); } - Position eyePos = dc.getView().getEyePosition(dc.getGlobe()); + Position eyePos = dc.getView().getEyePosition(); if (eyePos == null) return false; double altitude = eyePos.elevation; @@ -181,6 +181,41 @@ public void pick(DrawContext dc, Point point) { this.doPick(dc, point); } + public void preRender(DrawContext dc) + { + if (!this.enabled) + return; // Don't check for arg errors if we're disabled + + if (null == dc) + { + String message = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(message); + throw new IllegalStateException(message); + } + + if (null == dc.getGlobe()) + { + String message = Logging.getMessage("layers.AbstractLayer.NoGlobeSpecifiedInDrawingContext"); + Logging.error(message); + throw new IllegalStateException(message); + } + + if (null == dc.getView()) + { + String message = Logging.getMessage("layers.AbstractLayer.NoViewSpecifiedInDrawingContext"); + Logging.error(message); + throw new IllegalStateException(message); + } + + if (!this.isLayerActive(dc)) + return; + + if (!this.isLayerInView(dc)) + return; + + this.doPreRender(dc); + } + /** * @param dc * the current draw context @@ -228,6 +263,10 @@ protected void doPick(DrawContext dc, Point point) { // create an instance of the PickedObject and add to the dc via the dc.addPickedObject() method } + protected void doPreRender(DrawContext dc) + { + } + protected abstract void doRender(DrawContext dc); public void setExpiryTime(long expiryTime) { diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/layers/BasicTiledImageLayer.java b/WorldWindAndroid/src/gov/nasa/worldwind/layers/BasicTiledImageLayer.java index 0167f24..22f9abb 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/layers/BasicTiledImageLayer.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/layers/BasicTiledImageLayer.java @@ -5,6 +5,8 @@ */ package gov.nasa.worldwind.layers; +import gov.nasa.worldwind.WorldWind; +import gov.nasa.worldwind.WorldWindowImpl; import gov.nasa.worldwind.avlist.AVKey; import gov.nasa.worldwind.avlist.AVList; import gov.nasa.worldwind.avlist.AVListImpl; @@ -16,19 +18,20 @@ import gov.nasa.worldwind.geom.Sector; import gov.nasa.worldwind.ogc.wms.WMSCapabilities; import gov.nasa.worldwind.render.DrawContext; +import gov.nasa.worldwind.render.GpuTextureData; import gov.nasa.worldwind.render.GpuTextureTile; -import gov.nasa.worldwind.retrieve.BulkRetrievable; -import gov.nasa.worldwind.retrieve.BulkRetrievalThread; -import gov.nasa.worldwind.util.AbsentResourceList; -import gov.nasa.worldwind.util.DataConfigurationUtils; -import gov.nasa.worldwind.util.LevelSet; -import gov.nasa.worldwind.util.Logging; -import gov.nasa.worldwind.util.WWXML; +import gov.nasa.worldwind.retrieve.*; +import gov.nasa.worldwind.util.*; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; import java.net.URL; +import java.nio.ByteBuffer; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import org.w3c.dom.Document; -import org.w3c.dom.Element; /** * Edited By: Nicola Dorigatti, Trilogis @@ -45,7 +48,7 @@ public class BasicTiledImageLayer extends TiledImageLayer implements BulkRetriev protected static final int RESOURCE_ID_OGC_CAPABILITIES = 1; protected static final int DEFAULT_MAX_RESOURCE_ATTEMPTS = 3; protected static final int DEFAULT_MIN_RESOURCE_CHECK_INTERVAL = (int) 6e5; // 10 minutes - protected String textureFormat; + public BasicTiledImageLayer(LevelSet levelSet) { super(levelSet); @@ -58,17 +61,48 @@ public BasicTiledImageLayer(AVList params) { // TODO String[] strings = (String[]) params.getValue(AVKey.AVAILABLE_IMAGE_FORMATS); // if (strings != null && strings.length > 0) this.setAvailableImageFormats(strings); - String s = params.getStringValue(AVKey.TEXTURE_FORMAT); + String s = params.getStringValue(AVKey.DISPLAY_NAME); + if (s != null) this.setName(s); + + Double d = (Double) params.getValue(AVKey.OPACITY); + if (d != null) this.setOpacity(d); + + d = (Double) params.getValue(AVKey.MAX_ACTIVE_ALTITUDE); + if (d != null) this.setMaxActiveAltitude(d); + + d = (Double) params.getValue(AVKey.MIN_ACTIVE_ALTITUDE); + if (d != null) this.setMinActiveAltitude(d); + + d = (Double) params.getValue(AVKey.MAP_SCALE); + if (d != null) this.setValue(AVKey.MAP_SCALE, d); + + d = (Double) params.getValue(AVKey.DETAIL_HINT); + if (d != null) this.setDetailHint(d); + + s = params.getStringValue(AVKey.TEXTURE_FORMAT); if (s != null) this.setTextureFormat(s); Boolean b = (Boolean) params.getValue(AVKey.FORCE_LEVEL_ZERO_LOADS); if (b != null) this.setForceLevelZeroLoads(b); b = (Boolean) params.getValue(AVKey.RETAIN_LEVEL_ZERO_TILES); if (b != null) this.setRetainLevelZeroTiles(b); - // - // - // b = (Boolean) params.getValue(AVKey.USE_MIP_MAPS); - // if (b != null) this.setUseMipMaps(b); + + b = (Boolean) params.getValue(AVKey.NETWORK_RETRIEVAL_ENABLED); + if (b != null) this.setNetworkRetrievalEnabled(b); + + b = (Boolean) params.getValue(AVKey.USE_TRANSPARENT_TEXTURES); + if (b != null) this.setUseTransparentTextures(b); + + Object o = params.getValue(AVKey.URL_CONNECT_TIMEOUT); + if (o != null) this.setValue(AVKey.URL_CONNECT_TIMEOUT, o); + + o = params.getValue(AVKey.URL_READ_TIMEOUT); + if (o != null) this.setValue(AVKey.URL_READ_TIMEOUT, o); + + o = params.getValue(AVKey.RETRIEVAL_QUEUE_STALE_REQUEST_LIMIT); + if (o != null) this.setValue(AVKey.RETRIEVAL_QUEUE_STALE_REQUEST_LIMIT, o); + + if (params.getValue(AVKey.TRANSPARENCY_COLORS) != null) this.setValue(AVKey.TRANSPARENCY_COLORS, params.getValue(AVKey.TRANSPARENCY_COLORS)); this.setValue(AVKey.CONSTRUCTION_PARAMETERS, params.copy()); @@ -130,160 +164,7 @@ protected static void setFallbacks(AVList params) { if (params.getValue(AVKey.NUM_EMPTY_LEVELS) == null) params.setValue(AVKey.NUM_EMPTY_LEVELS, 0); } - protected void forceTextureLoad(GpuTextureTile tile) { - this.loadTile(tile); - } - - protected void requestTexture(DrawContext dc, GpuTextureTile tile) { - // Vec4 centroid = tile.getCentroidPoint(dc.getGlobe()); - // Vec4 referencePoint = this.getReferencePoint(dc); - // if (referencePoint != null) tile.setPriority(centroid.distanceTo3(referencePoint)); - // - // RequestTask task = this.createRequestTask(tile); - // this.getRequestQ().add(task); - Runnable task = this.createRequestTask(dc, tile); - if (task == null) { - String msg = Logging.getMessage("nullValue.TaskIsNull"); - Logging.warning(msg); - return; - } - - this.requestQ.add(task); - } - - protected RequestTask createRequestTask(DrawContext dc, GpuTextureTile tile) { - double priority = this.computeTilePriority(dc, tile); - tile.setPriority(priority); - return new RequestTask(tile, this, priority); - // return new RequestTask(tile, this); - } - - protected static class RequestTask implements Runnable, Comparable { - protected final BasicTiledImageLayer layer; - protected final GpuTextureTile tile; - protected double priority; - - protected RequestTask(GpuTextureTile tile, BasicTiledImageLayer layer, double priority) { - this.layer = layer; - this.tile = tile; - this.priority = priority; - } - - public void run() { - if (Thread.currentThread().isInterrupted()) return; // the task was cancelled because it's a duplicate or for some other reason - - this.layer.loadTile(this.tile); - } - - /** - * @param that - * the task to compare - * @return -1 if this less than that, 1 if greater than, 0 if equal - * @throws IllegalArgumentException - * if that is null - */ - public int compareTo(RequestTask that) { - if (that == null) return -1; - - return this.priority < that.priority ? -1 : (this.priority > that.priority ? 1 : 0); - } - - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof RequestTask)) return false; - - RequestTask that = (RequestTask) o; - return this.tile.equals(that.tile); - } - - public int hashCode() { - return (tile != null ? tile.hashCode() : 0); - } - - public String toString() { - return this.tile.toString(); - } - } - - /** - * Reads and returns the texture data at the specified URL, optionally converting it to the specified format and - * generating mip-maps. If textureFormat is a recognized mime type, this returns the texture data in - * the specified format. Otherwise, this returns the texture data in its native format. If useMipMaps is true, this generates mip maps for any - * non-DDS texture data, and uses any mip-maps contained in DDS texture - * data. - *

- * Supported texture formats are as follows: - *

    - *
  • image/dds - Returns DDS texture data, converting the data to DDS if necessary. If the data is already in DDS format it's returned as-is. - *
  • - *
- * - * @param url - * the URL referencing the texture data to read. - * @param textureFormat - * the texture data format to return. - * @param useMipMaps - * true to generate mip-maps for the texture data or use mip maps already in the texture data, - * and false to read the texture data without generating or using mip-maps. - * @return TextureData the texture data from the specified URL, in the specified format and with mip-maps. - * protected GpuTextureData readTexture(java.net.URL url, String textureFormat, boolean useMipMaps) { - * try { - * // If the caller has enabled texture compression, and the texture data is not a DDS file, then use read the - * // texture data and convert it to DDS. - * if ("image/dds".equalsIgnoreCase(textureFormat) && !url.toString().toLowerCase().endsWith("dds")) { - * // Configure a DDS compressor to generate mipmaps based according to the 'useMipMaps' parameter, and - * // convert the image URL to a compressed DDS format. - * DXTCompressionAttributes attributes = DDSCompressor.getDefaultCompressionAttributes(); - * attributes.setBuildMipmaps(useMipMaps); - * ByteBuffer buffer = DDSCompressor.compressImageURL(url, attributes); - * return GpuTextureData.createTextureData(WWIO.getInputStreamFromByteBuffer(buffer)); - * // return TextureIO.newTextureData(WWIO.getInputStreamFromByteBuffer(buffer), useMipMaps, null); - * } - * // If the caller has disabled texture compression, or if the texture data is already a DDS file, then read - * // the texture data without converting it. - * else { - * return null; // new DDSTextureReader().read(WWIO.getFileOrResourceAsStream(path, c)) - * // return TextureIO.newTextureData(url, useMipMaps, null); - * } - * } catch (Exception e) { - * String msg = Logging.getMessage("layers.TextureLayer.ExceptionAttemptingToReadTextureFile", url); - * Logging.error(msg, e); - * return null; - * } - * } - */ - - protected void addTileToCache(GpuTextureTile tile) { - getTextureTileCache().put(tile.getTileKey(), tile); - } - - /** - * Returns the format used to store images in texture memory, or null if - * images are stored in their native format. - * - * @return the texture image format; null if images are stored in their - * native format. - * @see #setTextureFormat(String) - */ - public String getTextureFormat() { - return this.textureFormat; - } - /** - * Specifies the format used to store images in texture memory, or null to - * store images in their native format. Suppported texture formats are as - * follows: - *
    - *
  • image/dds - Stores images in the compressed DDS format. If the image is already in DDS format it's stored as-is.
  • - *
- * - * @param textureFormat - * the texture image format; null to store images in their native - * format. - */ - public void setTextureFormat(String textureFormat) { - this.textureFormat = textureFormat; - } // *** Bulk download *** // *** Bulk download *** @@ -326,6 +207,49 @@ public BulkRetrievalThread makeLocal(Sector sector, double resolution, FileStore return thread; } + /** + * Start a new {@link BulkRetrievalThread} that downloads all imagery for a given sector and resolution to the + * current World Wind file cache, without downloading imagery that is already in the cache. + *

+ * This method creates and starts a thread to perform the download. A reference to the thread is returned. To create a downloader that has not been started, + * construct a {@link TiledImageLayerBulkDownloader}. + *

+ * Note that the target resolution must be provided in radians of latitude per texel, which is the resolution in meters divided by the globe radius. + * + * @param sector + * the sector to download imagery for. + * @param resolution + * the target resolution, provided in radians of latitude per texel. + * @param listener + * an optional retrieval listener. May be null. + * @return the {@link BulkRetrievalThread} executing the retrieval or null if the specified sector does + * not intersect the layer bounding sector. + * @throws IllegalArgumentException + * if the sector is null or the resolution is less than zero. + * @see TiledImageLayerBulkDownloader + */ + public BulkRetrievalThread makeLocal(Sector sector, double resolution, BulkRetrievalListener listener) { + return makeLocal(sector, resolution, null, listener); + } + + /** + * Get the estimated size in bytes of the imagery not in the World Wind file cache for the given sector and + * resolution. + *

+ * Note that the target resolution must be provided in radians of latitude per texel, which is the resolution in meters divided by the globe radius. + * + * @param sector + * the sector to estimate. + * @param resolution + * the target resolution, provided in radians of latitude per texel. + * @return the estimated size in bytes of the missing imagery. + * @throws IllegalArgumentException + * if the sector is null or the resolution is less than zero. + */ + public long getEstimatedMissingDataSize(Sector sector, double resolution) { + return this.getEstimatedMissingDataSize(sector, resolution, null); + } + /** * Get the estimated size in bytes of the imagery not in a specified file store for a specified sector and * resolution. @@ -354,6 +278,8 @@ public long getEstimatedMissingDataSize(Sector sector, double resolution, FileSt return downloader.getEstimatedMissingDataSize(); } + + // *** Tile download *** // *** Tile download *** @@ -510,6 +436,278 @@ protected void stopResourceRetrieval() { } } + // **************************************************************// + // ********************** Retrieval ***************************// + // **************************************************************// + + protected RequestTask createRequestTask(DrawContext dc, GpuTextureTile tile) { + double priority = this.computeTilePriority(dc, tile); + tile.setPriority(priority); + return new RequestTask(tile, this, priority); + } + + protected static class RequestTask implements Runnable, Comparable { + protected final BasicTiledImageLayer layer; + protected final GpuTextureTile tile; + protected double priority; + + protected RequestTask(GpuTextureTile tile, BasicTiledImageLayer layer, double priority) { + this.layer = layer; + this.tile = tile; + this.priority = priority; + } + + public void run() { + if (Thread.currentThread().isInterrupted()) return; // the task was cancelled because it's a duplicate or for some other reason + + this.layer.loadTile(this.tile); + } + + /** + * @param that + * the task to compare + * @return -1 if this less than that, 1 if greater than, 0 if equal + * @throws IllegalArgumentException + * if that is null + */ + public int compareTo(RequestTask that) { + if (that == null) return -1; + + return this.priority < that.priority ? -1 : (this.priority > that.priority ? 1 : 0); + } + + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof RequestTask)) return false; + + RequestTask that = (RequestTask) o; + return this.tile.equals(that.tile); + } + + public int hashCode() { + return (tile != null ? tile.hashCode() : 0); + } + + public String toString() { + return this.tile.toString(); + } + } + + + protected void addTileToCache(GpuTextureTile tile) { + GpuTextureTile.getMemoryCache().put(tile.getTileKey(), tile); + } + + protected void requestTile(DrawContext dc, GpuTextureTile tile) { + Runnable task = this.createRequestTask(dc, tile); + if (task == null) { + String msg = Logging.getMessage("nullValue.TaskIsNull"); + Logging.warning(msg); + return; + } + + this.requestQ.add(task); + } + + /** + * Compute the priority of loading this tile, based on distance from the eye to the tile's center point. Tiles + * closer to the eye have higher priority than those far from the eye. + * + * @param dc + * current draw context. + * @param tile + * tile for which to compute the priority. + * @return tile priority. A lower number indicates higher priority. + */ + protected double computeTilePriority(DrawContext dc, GpuTextureTile tile) { + // Tile priority is ordered from low (most priority) to high (least priority). Assign the tile priority based + // on square distance form the eye point. Since we don't care about the actual distance this enables us to + // avoid a square root computation. Tiles further from the eye point are loaded last. + return dc.getView().getEyePoint().distanceToSquared3(tile.getExtent().getCenter()); + } + + protected void forceTextureLoad(GpuTextureTile tile) { + this.loadTile(tile); + } + + /** + * Load a tile. If the tile exists in the file cache, it will be loaded from the file cache. If not, it will be + * requested from the network. + * + * @param tile + * tile to load. + */ + protected void loadTile(GpuTextureTile tile) { + URL textureURL = this.getDataFileStore().findFile(tile.getPath(), false); + if (textureURL != null) { + this.loadTexture(tile, textureURL); + } else { + this.retrieveTexture(tile, this.createDownloadPostProcessor(tile)); + } + } + + /** + * Load a tile from the file cache. + * + * @param tile + * tile to load. + * @param textureURL + * local URL to the cached resource. + */ + protected boolean loadTexture(GpuTextureTile tile, URL textureURL) { + GpuTextureData textureData; + + synchronized (this.fileLock) { + textureData = this.createTextureData(textureURL, this.getTextureFormat()); + } + + if (textureData != null) { + tile.setTextureData(textureData); + + // The tile's size has changed, so update its size in the memory cache. + if (tile.getLevelNumber() != 0 || !this.isRetainLevelZeroTiles()) + addTileToCache(tile); + + // Mark the tile as not absent to ensure that it is used, and cause any World Windows containing this layer + // to repaint themselves. + this.levels.unmarkResourceAbsent(tile); + this.firePropertyChange(AVKey.LAYER, null, this); + return true; + } else { + // Assume that something is wrong with the file and delete it. + this.getDataFileStore().removeFile(textureURL); + String message = Logging.getMessage("generic.DeletedCorruptDataFile", textureURL); + Logging.info(message); + return false; + } + } + + protected GpuTextureData createTextureData(URL textureURL, String textureFormat) { + return GpuTextureData.createTextureData(textureURL, textureURL.toString(), textureFormat, isUseMipMaps()); + } + + /** + * Create a post processor for a tile retrieval task. + * + * @param tile + * tile to create a post processor for. + * @return new post processor. + */ + protected DownloadPostProcessor createDownloadPostProcessor(GpuTextureTile tile) { + return new DownloadPostProcessor(tile, this, this.getDataFileStore()); + } + + /** + * Retrieve a tile from the network. This method initiates an asynchronous retrieval task and then returns. + * + * @param tile + * tile to download. + * @param postProcessor + * post processor to handle the retrieval. + */ + protected void retrieveTexture(GpuTextureTile tile, DownloadPostProcessor postProcessor) { + this.retrieveRemoteTexture(tile, postProcessor); + } + + protected void retrieveRemoteTexture(GpuTextureTile tile, DownloadPostProcessor postProcessor) { + if (!this.isNetworkRetrievalEnabled()) { + this.getLevels().markResourceAbsent(tile); + return; + } + + if (!WorldWind.getRetrievalService().isAvailable()) return; + + URL url; + try { + url = tile.getResourceURL(); + } catch (MalformedURLException e) { + Logging.error(Logging.getMessage("layers.TextureLayer.ExceptionCreatingTextureUrl", tile), e); + return; + } + + if (WorldWind.getNetworkStatus().isHostUnavailable(url)) { + this.getLevels().markResourceAbsent(tile); + return; + } + + Retriever retriever = URLRetriever.createRetriever(url, postProcessor); + if (retriever == null) { + Logging.error(Logging.getMessage("layers.TextureLayer.UnknownRetrievalProtocol", url.toString())); + return; + } + retriever.setValue(URLRetriever.EXTRACT_ZIP_ENTRY, "true"); // supports legacy layers + + // Apply any overridden timeouts. + Integer connectTimeout = AVListImpl.getIntegerValue(this, AVKey.URL_CONNECT_TIMEOUT); + if (connectTimeout != null && connectTimeout > 0) retriever.setConnectTimeout(connectTimeout); + + Integer readTimeout = AVListImpl.getIntegerValue(this, AVKey.URL_READ_TIMEOUT); + if (readTimeout != null && readTimeout > 0) retriever.setReadTimeout(readTimeout); + + Integer staleRequestLimit = AVListImpl.getIntegerValue(this, AVKey.RETRIEVAL_QUEUE_STALE_REQUEST_LIMIT); + if (staleRequestLimit != null && staleRequestLimit > 0) retriever.setStaleRequestLimit(staleRequestLimit); + + WorldWind.getRetrievalService().runRetriever(retriever, tile.getPriority()); + } + + protected static class DownloadPostProcessor extends AbstractRetrievalPostProcessor { + protected GpuTextureTile tile; + protected BasicTiledImageLayer layer; + protected FileStore fileStore; + + public DownloadPostProcessor(GpuTextureTile tile, BasicTiledImageLayer layer, FileStore fileStore) { + super(layer); + + this.tile = tile; + this.layer = layer; + this.fileStore = fileStore; + } + + @Override + protected void markResourceAbsent() { + this.layer.getLevels().markResourceAbsent(this.tile); + } + + @Override + protected Object getFileLock() { + return this.layer.fileLock; + } + + protected FileStore getFileStore() + { + return this.fileStore != null ? this.fileStore : this.layer.getDataFileStore(); + } + + @Override + protected File doGetOutputFile() { + return layer.getDataFileStore().newFile(this.tile.getPath()); + } + + @Override + protected ByteBuffer handleSuccessfulRetrieval() { + ByteBuffer buffer = super.handleSuccessfulRetrieval(); + + if (buffer != null) { + // We've successfully cached data. Check if there's a configuration file for this layer, create one + // if there's not. + //TODO write configurationFile + this.layer.writeConfigurationFile(this.getFileStore()); + + // Fire a property change to denote that the layer's backing data has changed. + this.layer.firePropertyChange(AVKey.LAYER, null, this); + } + + return buffer; + } + + @Override + protected ByteBuffer handleTextContent() throws IOException { + this.markResourceAbsent(); + + return super.handleTextContent(); + } + } + /** * Returns a Runnable task which retrieves any non-tile resources associated with a specified Layer in it's run * method. This task is used by the Layer to schedule periodic resource checks. If the task's run method throws an @@ -634,6 +832,9 @@ protected void writeConfigurationParams(FileStore fileStore, AVList params) { } protected void doWriteConfigurationParams(FileStore fileStore, String fileName, AVList params) { + if(WorldWindowImpl.DEBUG) + Logging.verbose(getName() + " writing layer configuration file"); + java.io.File file = fileStore.newFile(fileName); if (file == null) { String message = Logging.getMessage("generic.CannotCreateFile", fileName); diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/layers/CompassLayer.java b/WorldWindAndroid/src/gov/nasa/worldwind/layers/CompassLayer.java index df999dc..d555334 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/layers/CompassLayer.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/layers/CompassLayer.java @@ -5,8 +5,13 @@ */ package gov.nasa.worldwind.layers; +import android.graphics.Point; +import android.opengl.ETC1Util; +import android.opengl.GLES20; import gov.nasa.worldwind.BasicView; +import gov.nasa.worldwind.R; import gov.nasa.worldwind.View; +import gov.nasa.worldwind.WorldWindowImpl; import gov.nasa.worldwind.avlist.AVKey; import gov.nasa.worldwind.cache.GpuResourceCache; import gov.nasa.worldwind.exception.WWRuntimeException; @@ -16,12 +21,9 @@ import gov.nasa.worldwind.geom.Vec4; import gov.nasa.worldwind.pick.PickSupport; import gov.nasa.worldwind.pick.PickedObject; -import gov.nasa.worldwind.render.DrawContext; -import gov.nasa.worldwind.render.GpuProgram; -import gov.nasa.worldwind.render.GpuTexture; -import gov.nasa.worldwind.render.GpuTextureData; -import gov.nasa.worldwind.render.OrderedRenderable; +import gov.nasa.worldwind.render.*; import gov.nasa.worldwind.util.Logging; + import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -29,21 +31,19 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; -import android.graphics.Point; -import android.opengl.GLES20; /** * Edited By: Nicola Dorigatti, Trilogis - * + * * @author Nicola Dorigatti * @version $Id: CompassLayer.java 1 2013-08-08 $ */ public class CompassLayer extends AbstractLayer { - protected static final String VERTEX_SHADER_PATH_TEXTURE = "shaders/CompassLayerTexture.vert"; - protected static final String FRAGMENT_SHADER_PATH_TEXTURE = "shaders/CompassLayerTexture.frag"; - protected final Object programTextureKey = new Object(); - protected String iconFilePath = "images/notched-compass.png"; // TODO: make configurable + protected static final int VERTEX_SHADER_PATH_TEXTURE = R.raw.diffuse_tex_vert; + protected static final int FRAGMENT_SHADER_PATH_TEXTURE = R.raw.etc1alphafrag; + protected String iconFilePath; // TODO: make configurable protected double compassToViewportScale = 0.2; // TODO: make configurable + protected final Object programTextureKey = new Object(); protected double iconScale = 0.5; protected int borderWidth = 20; // TODO: make configurable protected String position = AVKey.NORTHEAST; // TODO: make configurable @@ -54,19 +54,28 @@ public class CompassLayer extends AbstractLayer { protected Vec4 locationOffset = null; protected boolean showTilt = true; protected PickSupport pickSupport = new PickSupport(); + private Matrix texMatrix = Matrix.fromIdentity(); // Draw it as ordered with an eye distance of 0 so that it shows up in front of most other things. protected OrderedIcon orderedImage = new OrderedIcon(); protected class OrderedIcon implements OrderedRenderable { + @Override + public Layer getLayer() { + return CompassLayer.this; + } + + @Override public double getDistanceFromEye() { return 0; } + @Override public void pick(DrawContext dc, Point pickPoint) { CompassLayer.this.draw(dc); } + @Override public void render(DrawContext dc) { CompassLayer.this.draw(dc); } @@ -85,17 +94,20 @@ public CompassLayer(String iconFilePath) { /** * Returns the layer's current icon file path. - * + * * @return the icon file path */ public String getIconFilePath() { + if(iconFilePath==null || iconFilePath.isEmpty()) + iconFilePath = ETC1Util.isETC1Supported() ? "images/notched-compass_mip_0.pkm" : + "images/notched-compass.png"; return iconFilePath; } /** * Sets the compass icon's image location. The layer first searches for this location in the current Java classpath. * If not found then the specified path is assumed to refer to the local file system. found there then the - * + * * @param iconFilePath * the path to the icon's image file */ @@ -110,7 +122,7 @@ public void setIconFilePath(String iconFilePath) { /** * Returns the layer's compass-to-viewport scale factor. - * + * * @return the compass-to-viewport scale factor */ public double getCompassToViewportScale() { @@ -122,7 +134,7 @@ public double getCompassToViewportScale() { * scale factor is used only when the layer's resize behavior is AVKey.RESIZE_STRETCH or AVKey.RESIZE_SHRINK_ONLY. * The icon's width is adjusted to occupy the proportion of the viewport's width indicated by this factor. The * icon's height is adjusted to maintain the compass image's native aspect ratio. - * + * * @param compassToViewportScale * the compass to viewport scale factor */ @@ -132,7 +144,7 @@ public void setCompassToViewportScale(double compassToViewportScale) { /** * Returns the icon scale factor. See {@link #setIconScale(double)} for a description of the scale factor. - * + * * @return the current icon scale */ public double getIconScale() { @@ -146,7 +158,7 @@ public double getIconScale() { * specified by {@link #setCompassToViewportScale(double)} and the current viewport size. *

* The default icon scale is 0.5. - * + * * @param iconScale * the icon scale factor */ @@ -156,7 +168,7 @@ public void setIconScale(double iconScale) { /** * Returns the compass icon's resize behavior. - * + * * @return the icon's resize behavior */ public String getResizeBehavior() { @@ -172,7 +184,7 @@ public String getResizeBehavior() { * compass-to-viewport scale and by the icon's image file size scaled by the current icon scale. If the value is * AVKey.RESIZE_SHRINK_ONLY (the default), icon sizing behaves as for AVKey.RESIZE_STRETCH but the icon will not * grow larger than the size specified in its image file scaled by the current icon scale. - * + * * @param resizeBehavior * the desired resize behavior */ @@ -186,7 +198,7 @@ public int getBorderWidth() { /** * Sets the compass icon offset from the viewport border. - * + * * @param borderWidth * the number of pixels to offset the compass icon from the borders indicated by {@link #setPosition(String)}. */ @@ -196,7 +208,7 @@ public void setBorderWidth(int borderWidth) { /** * Returns the current relative compass icon position. - * + * * @return the current compass position */ public String getPosition() { @@ -207,7 +219,7 @@ public String getPosition() { * Sets the relative viewport location to display the compass icon. Can be one of AVKey.NORTHEAST (the default), * AVKey.NORTHWEST, AVKey.SOUTHEAST, or AVKey.SOUTHWEST. These indicate the corner of the viewport to place the * icon. - * + * * @param position * the desired compass position */ @@ -222,7 +234,7 @@ public void setPosition(String position) { /** * Returns the current compass image location. - * + * * @return the current location center. May be null. */ public Vec4 getLocationCenter() { @@ -235,7 +247,7 @@ public Vec4 getLocationCenter() { * pixels. The origin is the window's lower left corner. Positive X values are to the right of the origin, positive * Y values are upwards from the origin. The final image location will be affected by the currently specified * location offset if a non-null location offset has been specified (see {@link #setLocationOffset(gov.nasa.worldwind.geom.Vec4)}). - * + * * @param locationCenter * the location center. May be null. * @see #setPosition(String) @@ -247,7 +259,7 @@ public void setLocationCenter(Vec4 locationCenter) { /** * Returns the current location offset. See #setLocationOffset for a description of the offset and its values. - * + * * @return the location offset. Will be null if no offset has been specified. */ public Vec4 getLocationOffset() { @@ -256,7 +268,7 @@ public Vec4 getLocationOffset() { /** * Specifies a placement offset from the compass' position on the screen. - * + * * @param locationOffset * the number of pixels to shift the compass image from its specified screen position. A * positive X value shifts the image to the right. A positive Y value shifts the image up. If @@ -289,6 +301,7 @@ protected void draw(DrawContext dc) { try { GLES20.glDisable(GLES20.GL_DEPTH_TEST); + WorldWindowImpl.glCheckError("glDisable"); double width = this.getScaledIconWidth(); double height = this.getScaledIconHeight(); @@ -312,7 +325,7 @@ protected void draw(DrawContext dc) { if (!dc.isPickingMode()) { modelview.multiplyAndSet(Matrix.fromTranslation(width / 2, height / 2, 0)); if (this.showTilt) // formula contributed by Ty Hayden - modelview.multiplyAndSet(Matrix.fromRotationX(Angle.fromDegrees(70d * (pitch / 90.0)))); + modelview.multiplyAndSet(Matrix.fromRotationX(Angle.fromDegrees(70d * (pitch / 90.0)))); modelview.multiplyAndSet(Matrix.fromRotationZ(Angle.fromDegrees(-heading))); modelview.multiplyAndSet(Matrix.fromTranslation(-width / 2, -height / 2, 0)); modelview.multiplyAndSet(Matrix.fromScale(width, height, 1d)); @@ -335,32 +348,49 @@ protected void draw(DrawContext dc) { if (iconTexture != null && textureProgram != null) { textureProgram.bind(); textureProgram.loadUniformMatrix("mvpMatrix", mvp); - GLES20.glEnable(GLES20.GL_TEXTURE_2D); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + WorldWindowImpl.glCheckError("glActiveTexture"); iconTexture.bind(); textureProgram.loadUniformSampler("sTexture", 0); + textureProgram.loadUniformSampler("aTexture", 1); + +// iconTexture.applyInternalTransform(dc, texMatrix); + textureProgram.loadUniformMatrix("texMatrix", texMatrix); + + textureProgram.loadUniform1f("uOpacity", this.getOpacity()); GLES20.glEnable(GLES20.GL_BLEND); + WorldWindowImpl.glCheckError("glEnable: GL_BLEND"); GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); + WorldWindowImpl.glCheckError("glBlendFunc"); float[] unitQuadVerts = new float[] { 0, 0, 1, 0, 1, 1, 0, 1 }; int pointLocation = textureProgram.getAttribLocation("vertexPoint"); GLES20.glEnableVertexAttribArray(pointLocation); + WorldWindowImpl.glCheckError("glEnableVertexAttribArray"); FloatBuffer vertexBuf = ByteBuffer.allocateDirect(unitQuadVerts.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); vertexBuf.put(unitQuadVerts); vertexBuf.rewind(); GLES20.glVertexAttribPointer(pointLocation, 2, GLES20.GL_FLOAT, false, 0, vertexBuf); + WorldWindowImpl.glCheckError("glVertexAttribPointer"); float[] textureVerts = new float[] { 0, 1, 1, 1, 1, 0, 0, 0 }; int textureLocation = textureProgram.getAttribLocation("aTextureCoord"); GLES20.glEnableVertexAttribArray(textureLocation); + WorldWindowImpl.glCheckError("glEnableVertexAttribArray"); FloatBuffer textureBuf = ByteBuffer.allocateDirect(textureVerts.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); textureBuf.put(textureVerts); textureBuf.rewind(); GLES20.glVertexAttribPointer(textureLocation, 2, GLES20.GL_FLOAT, false, 0, textureBuf); + WorldWindowImpl.glCheckError("glVertexAttribPointer"); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, unitQuadVerts.length / 2); + WorldWindowImpl.glCheckError("glDrawArrays"); GLES20.glDisableVertexAttribArray(pointLocation); + WorldWindowImpl.glCheckError("glDisableVertexAttribArray"); GLES20.glDisableVertexAttribArray(textureLocation); + WorldWindowImpl.glCheckError("glDisableVertexAttribArray"); GLES20.glUseProgram(0); + WorldWindowImpl.glCheckError("glUseProgram"); } } else { // Picking - XXX This else has not been tested, it could make rendering crash! Be aware! @@ -386,7 +416,8 @@ protected void draw(DrawContext dc) { // Draw the compass in the unique pick color. gl.glColor3ub((byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue()); GpuProgram textureProgram = this.getGpuProgram(dc.getGpuResourceCache(), programTextureKey, VERTEX_SHADER_PATH_TEXTURE, FRAGMENT_SHADER_PATH_TEXTURE); textureProgram.bind(); - + textureProgram.loadUniform1f("uOpacity", 1); + textureProgram.loadUniformMatrix("texMatrix", texMatrix); modelview.multiplyAndSet(Matrix.fromScale(width, height, 1d)); float[] unitQuadVerts = new float[] { 0, 0, 1, 0, 1, 1, 0, 1 }; FloatBuffer vertexBuf = ByteBuffer.allocateDirect(unitQuadVerts.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); @@ -394,10 +425,15 @@ protected void draw(DrawContext dc) { vertexBuf.rewind(); int pointLocation = textureProgram.getAttribLocation("vertexPoint"); GLES20.glEnableVertexAttribArray(pointLocation); + WorldWindowImpl.glCheckError("glEnableVertexAttribArray"); GLES20.glVertexAttribPointer(pointLocation, 2, GLES20.GL_FLOAT, false, 0, vertexBuf); + WorldWindowImpl.glCheckError("glVertexAttribPointer"); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, unitQuadVerts.length / 2); + WorldWindowImpl.glCheckError("glDrawArrays"); GLES20.glDisableVertexAttribArray(pointLocation); + WorldWindowImpl.glCheckError("glDisableVertexAttribArray"); GLES20.glUseProgram(0); + WorldWindowImpl.glCheckError("glUseProgram"); // dc.drawUnitQuad(); } finally { // Done picking @@ -409,8 +445,9 @@ protected void draw(DrawContext dc) { if (!dc.isPickingMode()) { GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); - GLES20.glDisable(GLES20.GL_TEXTURE_2D); // restore to default texture state + WorldWindowImpl.glCheckError("glBindTexture"); GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA); + WorldWindowImpl.glCheckError("glBlendFunc"); } } } @@ -512,7 +549,7 @@ protected double computePitch(DrawContext dc, View view) { return basicView.getLookAtTilt(dc.getGlobe()).degrees; } - protected GpuProgram getGpuProgram(GpuResourceCache cache, Object programKey, String shaderPath, String fragmentPath) { + protected GpuProgram getGpuProgram(GpuResourceCache cache, Object programKey, int shaderPath, int fragmentPath) { GpuProgram program = cache.getProgram(programKey); @@ -541,8 +578,8 @@ protected void initializeTexture(DrawContext dc) { iconStream = new FileInputStream(iconFile); } } - - iconTexture = GpuTexture.createTexture(dc, GpuTextureData.createTextureData(iconStream));// TextureIO.newTexture(iconStream, false, null); + String textureMime = ETC1Util.isETC1Supported() ? "image/pkm" : null; + iconTexture = GpuTexture.createTexture(dc, GpuTextureData.createTextureData(iconStream, getIconFilePath(), textureMime, true)); iconTexture.bind(); this.iconWidth = iconTexture.getWidth(); this.iconHeight = iconTexture.getHeight(); @@ -552,10 +589,6 @@ protected void initializeTexture(DrawContext dc) { Logging.error(msg); throw new WWRuntimeException(msg, e); } - // GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);// _MIPMAP_LINEAR); - // GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); - // GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); - // GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); } @Override diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/layers/Earth/BMNGOneImage.java b/WorldWindAndroid/src/gov/nasa/worldwind/layers/Earth/BMNGOneImage.java index 215d31d..c0d83d1 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/layers/Earth/BMNGOneImage.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/layers/Earth/BMNGOneImage.java @@ -16,7 +16,7 @@ */ public class BMNGOneImage extends AbstractLayer { - protected static final String IMAGE_PATH = "images/world.topo.bathy.200405.3x2048x1024.dds"; + protected static final String IMAGE_PATH = "images/world.topo.bathy.200405.3x2048x1024_mip_0.pkm"; protected SurfaceImage surfaceImage; diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/layers/Layer.java b/WorldWindAndroid/src/gov/nasa/worldwind/layers/Layer.java index 282c662..e340500 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/layers/Layer.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/layers/Layer.java @@ -94,6 +94,16 @@ public interface Layer extends WWObject, Disposable */ void pick(DrawContext dc, Point pickPoint); + /** + * Causes the layer to perform any actions necessary to subsequently render the layer. The layer has exclusive + * access to the frame buffer during the call, and may use it to generate images or other information that is + * subsequently used to render the layer's contents. Upon return, the OpenGL state must be restored to its + * original. + * + * @param dc the current draw context. + */ + void preRender(DrawContext dc); + /** * Cause the layer to draw its representation. * diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/layers/RenderableLayer.java b/WorldWindAndroid/src/gov/nasa/worldwind/layers/RenderableLayer.java index 0b28e78..6429e78 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/layers/RenderableLayer.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/layers/RenderableLayer.java @@ -194,6 +194,7 @@ public void removeAllRenderables() this.renderables.clear(); } + @Override protected void doPick(DrawContext dc, Point pickPoint) { // TODO: Determine whether maintaining a pick list here has any purpose. Is everything deferred to ordered rendering? @@ -235,6 +236,7 @@ protected void doPick(DrawContext dc, Point pickPoint) //} } + @Override protected void doRender(DrawContext dc) { for (Renderable renderable : this.renderables) @@ -252,6 +254,27 @@ protected void doRender(DrawContext dc) } } + @Override + protected void doPreRender(DrawContext dc) + { + for (Renderable renderable : renderables) + { + try + { + // If the caller has specified their own Iterable, + // then we cannot make any guarantees about its contents. + if (renderable != null && renderable instanceof PreRenderable) + ((PreRenderable) renderable).preRender(dc); + } + catch (Exception e) + { + String msg = Logging.getMessage("generic.ExceptionWhilePrerenderingRenderable"); + Logging.error(msg); + // continue to next renderable + } + } + } + /** * {@inheritDoc} *

diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/layers/ScalebarLayer.java b/WorldWindAndroid/src/gov/nasa/worldwind/layers/ScalebarLayer.java index d4a5d8b..f285cd9 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/layers/ScalebarLayer.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/layers/ScalebarLayer.java @@ -5,6 +5,9 @@ */ package gov.nasa.worldwind.layers; +import gov.nasa.worldwind.R; +import gov.nasa.worldwind.WorldWindowGLSurfaceView; +import gov.nasa.worldwind.WorldWindowImpl; import gov.nasa.worldwind.avlist.AVKey; import gov.nasa.worldwind.cache.GpuResourceCache; import gov.nasa.worldwind.geom.Angle; @@ -29,13 +32,13 @@ /** * Renders a scalebar graphic in a screen corner. * Edited By: Nicola Dorigatti, Trilogis - * + * * @author Patrick Murris * @version $Id: ScalebarLayer.java 508 2012-04-06 01:05:50Z tgaskins $ */ public class ScalebarLayer extends AbstractLayer { - protected static final String VERTEX_SHADER_PATH_COLOR = "shaders/ScalebarLayerColor.vert"; - protected static final String FRAGMENT_SHADER_PATH_COLOR = "shaders/ScalebarLayerColor.frag"; + protected static final int VERTEX_SHADER_PATH_COLOR = R.raw.simple_vert; + protected static final int FRAGMENT_SHADER_PATH_COLOR = R.raw.uniform_color_frag; // Units constants public final static String UNIT_METRIC = "gov.nasa.worldwind.ScalebarLayer.Metric"; public final static String UNIT_IMPERIAL = "gov.nasa.worldwind.ScalebarLayer.Imperial"; @@ -65,6 +68,11 @@ public class ScalebarLayer extends AbstractLayer { private TextRenderer textRenderer; private class OrderedIcon implements OrderedRenderable { + @Override + public Layer getLayer() { + return ScalebarLayer.this; + } + public double getDistanceFromEye() { return 0; } @@ -89,7 +97,7 @@ public ScalebarLayer() { /** * Get the apparent pixel size in meter at the reference position. - * + * * @return the apparent pixel size in meter at the reference position. */ public double getPixelSize() { @@ -98,7 +106,7 @@ public double getPixelSize() { /** * Get the scalebar graphic Dimension (in pixels) - * + * * @return the scalebar graphic Dimension */ public Rect getSize() { @@ -107,7 +115,7 @@ public Rect getSize() { /** * Set the scalebar graphic Dimenion (in pixels) - * + * * @param size * the scalebar graphic Dimension */ @@ -122,7 +130,7 @@ public void setSize(Rect size) { /** * Get the scalebar color - * + * * @return the scalebar Color */ public float[] getColor() { @@ -131,7 +139,7 @@ public float[] getColor() { /** * Set the scalbar Color - * + * * @param color * the scalebar Color */ @@ -146,7 +154,7 @@ public void setColor(float[] color) { /** * Returns the scalebar-to-viewport scale factor. - * + * * @return the scalebar-to-viewport scale factor */ public double getToViewportScale() { @@ -161,7 +169,7 @@ public double getToViewportScale() { * proportion of the viewport's width indicated by this factor. The * scalebar's height is adjusted to maintain the scalebar's Dimension aspect * ratio. - * + * * @param toViewportScale * the scalebar to viewport scale factor */ @@ -177,7 +185,7 @@ public String getPosition() { * Sets the relative viewport location to display the scalebar. Can be one * of AVKey.NORTHEAST, AVKey.NORTHWEST, AVKey.SOUTHEAST (the default), or * AVKey.SOUTHWEST. These indicate the corner of the viewport. - * + * * @param position * the desired scalebar position */ @@ -192,7 +200,7 @@ public void setPosition(String position) { /** * Returns the current scalebar center location. - * + * * @return the current location center. May be null. */ public Vec4 getLocationCenter() { @@ -207,7 +215,7 @@ public Vec4 getLocationCenter() { * origin, positive Y values are upwards from the origin. The final scalebar * location will be affected by the currently specified location offset if a * non-null location offset has been specified (see #setLocationOffset). - * + * * @param locationCenter * the scalebar center. May be null. * @see #setPosition @@ -220,7 +228,7 @@ public void setLocationCenter(Vec4 locationCenter) { /** * Returns the current location offset. See #setLocationOffset for a * description of the offset and its values. - * + * * @return the location offset. Will be null if no offset has been * specified. */ @@ -230,7 +238,7 @@ public Vec4 getLocationOffset() { /** * Specifies a placement offset from the scalebar's position on the screen. - * + * * @param locationOffset * the number of pixels to shift the scalebar from its specified * screen position. A positive X value shifts the image to the @@ -245,7 +253,7 @@ public void setLocationOffset(Vec4 locationOffset) { /** * Returns the layer's resize behavior. - * + * * @return the layer's resize behavior */ public String getResizeBehavior() { @@ -263,7 +271,7 @@ public String getResizeBehavior() { * enlarges. If the value is AVKey.RESIZE_SHRINK_ONLY (the default), * scalebar sizing behaves as for AVKey.RESIZE_STRETCH but it will not grow * larger than the size specified in its Dimension. - * + * * @param resizeBehavior * the desired resize behavior */ @@ -277,7 +285,7 @@ public int getBorderWidth() { /** * Sets the scalebar offset from the viewport border. - * + * * @param borderWidth * the number of pixels to offset the scalebar from the borders * indicated by {@link #setPosition(String)}. @@ -292,7 +300,7 @@ public String getUnit() { /** * Sets the unit the scalebar uses to display distances. Can be one of {@link #UNIT_METRIC} (the default), or {@link #UNIT_IMPERIAL}. - * + * * @param unit * the desired unit */ @@ -302,7 +310,7 @@ public void setUnit(String unit) { /** * Get the scalebar legend Fon - * + * * @return the scalebar legend Font */ public Paint getPaint() { @@ -311,8 +319,8 @@ public Paint getPaint() { /** * Set the scalebar legend Fon - * - * @param font + * + * @param paint * the scalebar legend Font */ public void setPaint(Paint paint) { @@ -343,6 +351,7 @@ public void draw(DrawContext dc) { try { GLES20.glDisable(GLES20.GL_DEPTH_TEST); + WorldWindowImpl.glCheckError("glDisable(GL_DEPTH_TEST)"); double width = this.size.width; double height = this.size.height; @@ -402,18 +411,25 @@ public void draw(DrawContext dc) { // Draw scale if (!dc.isPickingMode()) { GLES20.glEnable(GLES20.GL_BLEND); + + WorldWindowImpl.glCheckError("glEnable"); GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); + + WorldWindowImpl.glCheckError("glBlendFunc"); GpuProgram colorProgram = this.getGpuProgram(dc.getGpuResourceCache(), programColorKey, VERTEX_SHADER_PATH_COLOR, FRAGMENT_SHADER_PATH_COLOR); if (colorProgram != null) { // Set color using current layer opacity float[] backColor = this.getBackgroundColor(this.color); colorProgram.bind(); - colorProgram.loadUniform4f("uColor", backColor[0], backColor[1], backColor[2], backColor[3] * this.getOpacity()); + colorProgram.loadUniform1f("uOpacity", getOpacity()); + colorProgram.loadUniform4f("uColor", backColor[0], backColor[1], backColor[2], backColor[3]); modelview.multiplyAndSet(Matrix.fromTranslation((width - divWidth) / 2, 0d, 0d)); Matrix mvp = Matrix.fromIdentity().multiplyAndSet(projection, modelview); colorProgram.loadUniformMatrix("mvpMatrix", mvp); int pointLocation = colorProgram.getAttribLocation("vertexPoint"); GLES20.glEnableVertexAttribArray(pointLocation); + + WorldWindowImpl.glCheckError("glEnableVertexAttribArray"); this.drawScale(dc, divWidth, height, pointLocation); colorProgram.loadUniform4f("uColor", color[0], color[1], color[2], this.getOpacity()); @@ -447,10 +463,12 @@ public void draw(DrawContext dc) { } } finally { GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA); + + WorldWindowImpl.glCheckError("glBlendFunc"); } } - protected GpuProgram getGpuProgram(GpuResourceCache cache, Object programKey, String shaderPath, String fragmentPath) { + protected GpuProgram getGpuProgram(GpuResourceCache cache, Object programKey, int shaderPath, int fragmentPath) { GpuProgram program = cache.getProgram(programKey); @@ -502,11 +520,19 @@ private void drawScale(DrawContext dc, double width, double height, int pointLoc float[] verts = new float[] { 0, (float) height, 0, 0, 0, 0, (float) width, 0, 0, (float) width, (float) height, 0 }; FloatBuffer vertBuf = createBuffer(verts); GLES20.glVertexAttribPointer(pointLocation, 3, GLES20.GL_FLOAT, false, 0, vertBuf); + + WorldWindowImpl.glCheckError("glVertexAttribPointer"); GLES20.glDrawArrays(GLES20.GL_LINE_STRIP, 0, verts.length / 3); + + WorldWindowImpl.glCheckError("glDrawArrays"); verts = new float[] { (float) (width / 2), 0, 0, (float) (width / 2), (float) (height / 2), 0 }; vertBuf = createBuffer(verts); GLES20.glVertexAttribPointer(pointLocation, 3, GLES20.GL_FLOAT, false, 0, vertBuf); + + WorldWindowImpl.glCheckError("glVertexAttribPointer"); GLES20.glDrawArrays(GLES20.GL_LINE_STRIP, 0, verts.length / 3); + + WorldWindowImpl.glCheckError("glDrawArrays"); } protected FloatBuffer createBuffer(float[] array) { diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/layers/SkyGradientLayer.java b/WorldWindAndroid/src/gov/nasa/worldwind/layers/SkyGradientLayer.java index df03038..f66cd73 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/layers/SkyGradientLayer.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/layers/SkyGradientLayer.java @@ -5,14 +5,13 @@ */ package gov.nasa.worldwind.layers; -import gov.nasa.worldwind.View; +import gov.nasa.worldwind.*; import gov.nasa.worldwind.cache.GpuResourceCache; import gov.nasa.worldwind.geom.Angle; import gov.nasa.worldwind.geom.Matrix; import gov.nasa.worldwind.geom.Position; import gov.nasa.worldwind.geom.Vec4; -import gov.nasa.worldwind.render.DrawContext; -import gov.nasa.worldwind.render.GpuProgram; +import gov.nasa.worldwind.render.*; import gov.nasa.worldwind.util.Logging; import gov.nasa.worldwind.util.WWMath; import java.nio.ByteBuffer; @@ -26,13 +25,13 @@ *

* Note : based on a spherical globe.
* Issue : Ellipsoidal globe doesnt match the spherical atmosphere everywhere. Edited By: Nicola Dorigatti, Trilogis - * + * * @author Patrick Murris * @version $Id: SkyGradientLayer.java 1 2011-07-16 23:22:47Z dcollins $ */ public class SkyGradientLayer extends AbstractLayer { - protected static final String VERTEX_SHADER_PATH = "shaders/SkyGradientLayer.vert"; - protected static final String FRAGMENT_SHADER_PATH = "shaders/SkyGradientLayer.frag"; + protected static final int VERTEX_SHADER_PATH = R.raw.vertex_color_vert; + protected static final int FRAGMENT_SHADER_PATH = R.raw.vertex_color_frag; protected final static int STACKS = 12; protected final static int SLICES = 64; @@ -54,7 +53,7 @@ public SkyGradientLayer() { /** * Get the atmosphere thickness in meter - * + * * @return the atmosphere thickness in meter */ public double getAtmosphereThickness() { @@ -63,7 +62,7 @@ public double getAtmosphereThickness() { /** * Set the atmosphere thickness in meter - * + * * @param thickness * the atmosphere thickness in meter */ @@ -79,7 +78,7 @@ public void setAtmosphereThickness(double thickness) { /** * Get the horizon color - * + * * @return the horizon color */ public float[] getHorizonColor() { @@ -88,7 +87,7 @@ public float[] getHorizonColor() { /** * Set the horizon color - * + * * @param color * the horizon color */ @@ -104,7 +103,7 @@ public void setHorizonColor(float[] color) { /** * Get the zenith color - * + * * @return the zenith color */ public float[] getZenithColor() { @@ -113,7 +112,7 @@ public float[] getZenithColor() { /** * Set the zenith color - * + * * @param color * the zenith color */ @@ -142,11 +141,12 @@ public void doRender(DrawContext dc) { program.bind(); if (!this.isValid(dc)) vertexArrays = this.updateSkyDome(dc); GLES20.glDisable(GLES20.GL_CULL_FACE); + WorldWindowImpl.glCheckError("glDisable: GL_CULL_FACE"); GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); - // GLES20.glDisable(GLES20.GL_DEPTH_TEST); - // GLES20.glDepthMask(false); - // GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); - // GLES20.glEnable(GLES20.GL_BLEND); + WorldWindowImpl.glCheckError("glBlendFunc"); + + program.loadUniformColor("uColor", Color.white()); + program.loadUniform1f("uOpacity", this.getOpacity()); Matrix projection = this.createProjectionMatrix(dc); // this.applyDrawProjection(dc); @@ -158,8 +158,11 @@ public void doRender(DrawContext dc) { this.drawVertexArrays(dc, vertexArrays, program); } finally { GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA); + + WorldWindowImpl.glCheckError("glBlendFunc"); GLES20.glEnable(GLES20.GL_CULL_FACE); - // GLES20.glDisable(GLES20.GL_BLEND); + + WorldWindowImpl.glCheckError("glEnable: GL_CULL_FACE"); } } @@ -186,8 +189,12 @@ protected GpuProgram getGpuProgram(GpuResourceCache cache) { protected void drawVertexArrays(DrawContext dc, ArrayList vertexArrays, GpuProgram program) { int pointLocation = program.getAttribLocation("vertexPoint"); GLES20.glEnableVertexAttribArray(pointLocation); + WorldWindowImpl.glCheckError("glEnableVertexAttribArray"); + int colorLocation = program.getAttribLocation("vertexColor"); GLES20.glEnableVertexAttribArray(colorLocation); + WorldWindowImpl.glCheckError("glEnableVertexAttribArray"); + for (int i = 0; i < vertexArrays.size(); i = i + 2) { float[] vertexArray = vertexArrays.get(i); float[] colorArray = vertexArrays.get(i + 1); @@ -195,14 +202,22 @@ protected void drawVertexArrays(DrawContext dc, ArrayList vertexArrays, vertexBuf.put(vertexArray); vertexBuf.rewind(); GLES20.glVertexAttribPointer(pointLocation, 3, GLES20.GL_FLOAT, false, 0, vertexBuf); + WorldWindowImpl.glCheckError("glVertexAttribPointer"); + FloatBuffer colorBuf = ByteBuffer.allocateDirect(colorArray.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); colorBuf.put(colorArray); colorBuf.rewind(); GLES20.glVertexAttribPointer(colorLocation, 4, GLES20.GL_FLOAT, false, 0, colorBuf); + WorldWindowImpl.glCheckError("glVertexAttribPointer"); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, vertexArray.length / 3); + WorldWindowImpl.glCheckError("glDrawArrays"); } GLES20.glDisableVertexAttribArray(pointLocation); + WorldWindowImpl.glCheckError("glDisableVertexAttribArray"); + GLES20.glDisableVertexAttribArray(colorLocation); + WorldWindowImpl.glCheckError("glDisableVertexAttribArray"); } protected Matrix createModelViewMatrix(DrawContext dc) { @@ -230,7 +245,7 @@ protected Matrix createProjectionMatrix(DrawContext dc) { if (viewportWidth <= 0) viewportWidth = 1; if (viewportHeight <= 0) viewportHeight = 1; - double horizonDist = WWMath.computeHorizonDistance(dc.getGlobe(), view.getEyePosition(dc.getGlobe()).elevation); + double horizonDist = WWMath.computeHorizonDistance(dc.getGlobe(), view.getEyePosition().elevation); Matrix projection = Matrix.fromPerspective(view.getFieldOfView(), viewportWidth, viewportHeight, 100, horizonDist + 10e3); return projection; } @@ -239,7 +254,7 @@ protected ArrayList updateSkyDome(DrawContext dc) { View view = dc.getView(); ArrayList retval = null; - double tangentialDistance = WWMath.computeHorizonDistance(dc.getGlobe(), view.getEyePosition(dc.getGlobe()).elevation); + double tangentialDistance = WWMath.computeHorizonDistance(dc.getGlobe(), view.getEyePosition().elevation); double distToCenterOfPlanet = view.getEyePoint().getLength3(); Position camPos = dc.getGlobe().computePositionFromPoint(view.getEyePoint()); double worldRadius = dc.getGlobe().computePointFromPosition(camPos, 0).getLength3(); @@ -273,7 +288,7 @@ protected ArrayList updateSkyDome(DrawContext dc) { /** * Draws the sky dome - * + * * @param dc * the current DrawContext * @param radius @@ -407,7 +422,7 @@ protected ArrayList computeSkyDome(DrawContext dc, float radius, double /** * Converts position in spherical coordinates (lat/lon/altitude) to cartesian (XYZ) coordinates. - * + * * @param latitude * Latitude in decimal degrees * @param longitude @@ -427,7 +442,7 @@ protected static Vec4 SphericalToCartesian(double latitude, double longitude, do /** * Converts position in cartesian coordinates (XYZ) to spherical (radius, lat, lon) coordinates. - * + * * @param x * X coordinate * @param y diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/layers/TiledImageLayer.java b/WorldWindAndroid/src/gov/nasa/worldwind/layers/TiledImageLayer.java index 49dd7fe..e16447d 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/layers/TiledImageLayer.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/layers/TiledImageLayer.java @@ -5,50 +5,32 @@ */ package gov.nasa.worldwind.layers; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Point; +import android.opengl.GLES20; import gov.nasa.worldwind.Configuration; import gov.nasa.worldwind.WorldWind; +import gov.nasa.worldwind.WorldWindowImpl; import gov.nasa.worldwind.avlist.AVKey; import gov.nasa.worldwind.avlist.AVList; import gov.nasa.worldwind.avlist.AVListImpl; import gov.nasa.worldwind.cache.BasicMemoryCache; import gov.nasa.worldwind.cache.FileStore; import gov.nasa.worldwind.cache.MemoryCache; -import gov.nasa.worldwind.event.BulkRetrievalListener; -import gov.nasa.worldwind.geom.Angle; -import gov.nasa.worldwind.geom.Extent; -import gov.nasa.worldwind.geom.LatLon; -import gov.nasa.worldwind.geom.Position; -import gov.nasa.worldwind.geom.Rect; -import gov.nasa.worldwind.geom.Sector; -import gov.nasa.worldwind.geom.Vec4; +import gov.nasa.worldwind.geom.*; import gov.nasa.worldwind.render.DrawContext; -import gov.nasa.worldwind.render.GpuTextureData; import gov.nasa.worldwind.render.GpuTextureTile; -import gov.nasa.worldwind.retrieve.AbstractRetrievalPostProcessor; -import gov.nasa.worldwind.retrieve.BulkRetrievable; -import gov.nasa.worldwind.retrieve.BulkRetrievalThread; -import gov.nasa.worldwind.retrieve.Retriever; -import gov.nasa.worldwind.retrieve.URLRetriever; -import gov.nasa.worldwind.util.DataConfigurationUtils; -import gov.nasa.worldwind.util.Level; -import gov.nasa.worldwind.util.LevelSet; -import gov.nasa.worldwind.util.Logging; -import gov.nasa.worldwind.util.Tile; -import gov.nasa.worldwind.util.TileKey; -import gov.nasa.worldwind.util.WWIO; -import gov.nasa.worldwind.util.WWXML; -import java.io.File; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.ByteBuffer; +import gov.nasa.worldwind.render.Renderable; +import gov.nasa.worldwind.render.TextRenderer; +import gov.nasa.worldwind.util.*; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.xml.xpath.XPath; import java.util.ArrayList; import java.util.List; import java.util.concurrent.PriorityBlockingQueue; -import javax.xml.xpath.XPath; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import android.graphics.Point; /** * Edited By: Nicola Dorigatti, Trilogis @@ -57,7 +39,7 @@ * @version $Id: TiledImageLayer.java 842 2012-10-09 23:46:47Z tgaskins $ */ // TODO: apply layer opacity during rendering -public class TiledImageLayer extends AbstractLayer implements Tile.TileFactory, BulkRetrievable { +public abstract class TiledImageLayer extends AbstractLayer implements Tile.TileFactory { protected LevelSet levels; protected double detailHintOrigin = 2.6; // the default detail hint origin protected double detailHint; @@ -65,7 +47,11 @@ public class TiledImageLayer extends AbstractLayer implements Tile.TileFactory, protected boolean levelZeroLoaded = false; protected boolean retainLevelZeroTiles = false; protected boolean useTransparentTextures = false; - protected List topLevelTiles = new ArrayList(); + protected boolean useMipMaps = true; + protected String textureFormat; + private boolean drawTileIDs = false; + private boolean drawBoundingVolumes = false; + protected List topLevelTiles = new ArrayList(); protected String tileCountName; // Stuff computed each frame @@ -73,6 +59,10 @@ public class TiledImageLayer extends AbstractLayer implements Tile.TileFactory, protected GpuTextureTile currentAncestorTile; protected PriorityBlockingQueue requestQ = new PriorityBlockingQueue(200); protected final Object fileLock = new Object(); + protected TextRenderer textRenderer; + + abstract protected void forceTextureLoad(GpuTextureTile tile); + abstract protected void requestTile(DrawContext dc, GpuTextureTile tile); public TiledImageLayer(AVList params) { if (params == null) { @@ -85,46 +75,7 @@ public TiledImageLayer(AVList params) { this.setValue(AVKey.SECTOR, this.levels.getSector()); this.setPickEnabled(false); // textures are assumed to be terrain unless specifically indicated otherwise. - this.tileCountName = this.getName() + " Tiles"; - - String s = params.getStringValue(AVKey.DISPLAY_NAME); - if (s != null) this.setName(s); - - Double d = (Double) params.getValue(AVKey.OPACITY); - if (d != null) this.setOpacity(d); - - d = (Double) params.getValue(AVKey.MAX_ACTIVE_ALTITUDE); - if (d != null) this.setMaxActiveAltitude(d); - - d = (Double) params.getValue(AVKey.MIN_ACTIVE_ALTITUDE); - if (d != null) this.setMinActiveAltitude(d); - - d = (Double) params.getValue(AVKey.MAP_SCALE); - if (d != null) this.setValue(AVKey.MAP_SCALE, d); - - d = (Double) params.getValue(AVKey.DETAIL_HINT); - if (d != null) this.setDetailHint(d); - - Boolean b; - - b = (Boolean) params.getValue(AVKey.NETWORK_RETRIEVAL_ENABLED); - if (b != null) this.setNetworkRetrievalEnabled(b); - - b = (Boolean) params.getValue(AVKey.USE_TRANSPARENT_TEXTURES); - if (b != null) this.setUseTransparentTextures(b); - - Object o = params.getValue(AVKey.URL_CONNECT_TIMEOUT); - if (o != null) this.setValue(AVKey.URL_CONNECT_TIMEOUT, o); - - o = params.getValue(AVKey.URL_READ_TIMEOUT); - if (o != null) this.setValue(AVKey.URL_READ_TIMEOUT, o); - - o = params.getValue(AVKey.RETRIEVAL_QUEUE_STALE_REQUEST_LIMIT); - if (o != null) this.setValue(AVKey.RETRIEVAL_QUEUE_STALE_REQUEST_LIMIT, o); - - if (params.getValue(AVKey.TRANSPARENCY_COLORS) != null) this.setValue(AVKey.TRANSPARENCY_COLORS, params.getValue(AVKey.TRANSPARENCY_COLORS)); - - this.setValue(AVKey.CONSTRUCTION_PARAMETERS, params.copy()); + setName(this.getName()); } public TiledImageLayer(Element domElement, AVList params) { @@ -156,7 +107,7 @@ protected static void setFallbacks(AVList params) { if (params.getValue(AVKey.TILE_HEIGHT) == null) params.setValue(AVKey.TILE_HEIGHT, 512); - if (params.getValue(AVKey.FORMAT_SUFFIX) == null) params.setValue(AVKey.FORMAT_SUFFIX, ".dds"); + if (params.getValue(AVKey.FORMAT_SUFFIX) == null) params.setValue(AVKey.FORMAT_SUFFIX, ".png"); if (params.getValue(AVKey.NUM_LEVELS) == null) params.setValue(AVKey.NUM_LEVELS, 19); // approximately 0.1 meters per pixel @@ -178,6 +129,13 @@ public Object getValue(String key) { return value != null ? value : this.getLevels().getValue(key); // see if the level set has it } + @Override + public void setName(String name) + { + super.setName(name); + this.tileCountName = this.getName() + " Tiles"; + } + public boolean isForceLevelZeroLoads() { return this.forceLevelZeroLoads; } @@ -194,6 +152,26 @@ public void setRetainLevelZeroTiles(boolean retainLevelZeroTiles) { this.retainLevelZeroTiles = retainLevelZeroTiles; } + public boolean isDrawTileIDs() + { + return drawTileIDs; + } + + public void setDrawTileIDs(boolean drawTileIDs) + { + this.drawTileIDs = drawTileIDs; + } + + public boolean isDrawBoundingVolumes() + { + return drawBoundingVolumes; + } + + public void setDrawBoundingVolumes(boolean drawBoundingVolumes) + { + this.drawBoundingVolumes = drawBoundingVolumes; + } + /** * Indicates the layer's detail hint, which is described in {@link #setDetailHint(double)}. * @@ -241,6 +219,44 @@ public void setUseTransparentTextures(boolean useTransparentTextures) { this.useTransparentTextures = useTransparentTextures; } + /** + * Returns the format used to store images in texture memory, or null if + * images are stored in their native format. + * + * @return the texture image format; null if images are stored in their + * native format. + * @see #setTextureFormat(String) + */ + public String getTextureFormat() { + return this.textureFormat; + } + + /** + * Specifies the format used to store images in texture memory, or null to + * store images in their native format. Suppported texture formats are as + * follows: + *

    + *
  • image/dds - Stores images in the compressed DDS format. If the image is already in DDS format it's stored as-is.
  • + *
+ * + * @param textureFormat + * the texture image format; null to store images in their native + * format. + */ + public void setTextureFormat(String textureFormat) { + this.textureFormat = textureFormat; + } + + public boolean isUseMipMaps() + { + return useMipMaps; + } + + public void setUseMipMaps(boolean useMipMaps) + { + this.useMipMaps = useMipMaps; + } + /** * Specifies the time of the layer's most recent dataset update, beyond which cached data is invalid. If greater * than zero, the layer ignores and eliminates any in-memory or on-disk cached data older than the time specified, @@ -317,6 +333,9 @@ protected Vec4 getReferencePoint(DrawContext dc) { @Override protected void doRender(DrawContext dc) { + if (this.forceLevelZeroLoads && !this.levelZeroLoaded) + this.loadAllTopLevelTextures(dc); + if (dc.getSurfaceGeometry() == null || dc.getSurfaceGeometry().size() < 1) return; this.assembleTiles(dc); @@ -324,8 +343,17 @@ protected void doRender(DrawContext dc) { if (!this.currentTiles.isEmpty()) { // TODO: apply opacity and transparent texture support + //TODO Draw Tile boundries/labels + dc.setPerFrameStatistic(PerformanceStatistic.IMAGE_TILE_COUNT, this.tileCountName, + this.currentTiles.size()); dc.getSurfaceTileRenderer().renderTiles(dc, this.currentTiles); + if (this.drawTileIDs) + this.drawTileIDs(dc, this.currentTiles); + + if (this.drawBoundingVolumes) + this.drawBoundingVolumes(dc, this.currentTiles); + // Check texture expiration. Memory-cached textures are checked for expiration only when an explicit, // non-zero expiry time has been set for the layer. If none has been set, the expiry times of the layer's // individual levels are used, but only for images in the local file cache, not textures in memory. This is @@ -341,6 +369,46 @@ protected void doRender(DrawContext dc) { // TODO: clear fallback tiles } + protected void drawTileIDs(DrawContext dc, List tiles) + { + Paint paint = new Paint(); + paint.setColor(Color.YELLOW); + if(textRenderer==null) { + textRenderer = new TextRenderer(dc, paint); + } + + GLES20.glDisable(GLES20.GL_DEPTH_TEST); + GLES20.glDepthMask(false); + + for (GpuTextureTile tile : tiles) + { + String tileLabel = tile.getLabel(); + + if (tile.getFallbackTile() != null) + tileLabel += "/" + tile.getFallbackTile().getLabel(); + + LatLon ll = tile.getSector().getCentroid(); + Vec4 pt = new Vec4(); + dc.getView().project(dc.getGlobe().computePointFromPosition(ll.getLatitude(), ll.getLongitude(), + dc.getGlobe().getElevation(ll.getLatitude(), ll.getLongitude())), pt); + textRenderer.draw(tileLabel, (int) pt.x, (int) pt.y); + } + GLES20.glEnable(GLES20.GL_DEPTH_TEST); + GLES20.glDepthMask(true); + } + + protected void drawBoundingVolumes(DrawContext dc, List tiles) + { + for (GpuTextureTile tile : tiles) + { + if (tile.getExtent() instanceof Renderable) + ((Renderable) tile.getExtent()).render(dc); + } + + Box c = Sector.computeBoundingBox(dc.getGlobe(), dc.getVerticalExaggeration(), this.levels.getSector()); + c.render(dc); + } + public GpuTextureTile createTile(Sector sector, Level level, int row, int column) { if (sector == null) { String msg = Logging.getMessage("nullValue.SectorIsNull"); @@ -366,9 +434,23 @@ public GpuTextureTile createTile(Sector sector, Level level, int row, int column throw new IllegalArgumentException(msg); } - return new GpuTextureTile(sector, level, row, column, this.getTextureTileCache()); + return new GpuTextureTile(sector, level, row, column); } + protected void loadAllTopLevelTextures(DrawContext dc) + { + if(WorldWindowImpl.DEBUG) + Logging.verbose(getName() + "- Creating Top Level Tiles"); + for (GpuTextureTile tile : this.getTopLevels()) + { + if (!tile.isTextureInMemory(dc.getTextureCache())) + this.forceTextureLoad(tile); + } + + this.levelZeroLoaded = true; + } + + // ============== Tile Assembly ======================= // // ============== Tile Assembly ======================= // // ============== Tile Assembly ======================= // @@ -388,6 +470,15 @@ protected void assembleTiles(DrawContext dc) { } } + public List getTopLevels() + { + if (this.topLevelTiles == null) + this.createTopLevelTiles(); + + return topLevelTiles; + } + + protected void createTopLevelTiles() { if (this.levels.getFirstLevel() == null) { Logging.warning(Logging.getMessage("generic.FirstLevelIsNull")); @@ -417,7 +508,7 @@ protected void addTileOrDescendants(DrawContext dc, GpuTextureTile tile) { // transform is applied during rendering to align the sector's texture coordinates with the appropriate region // of the ancestor's texture. - MemoryCache cache = this.getTextureTileCache(); + MemoryCache cache = GpuTextureTile.getMemoryCache(); GpuTextureTile ancestorTile = null; try { @@ -425,7 +516,19 @@ protected void addTileOrDescendants(DrawContext dc, GpuTextureTile tile) { ancestorTile = this.currentAncestorTile; this.currentAncestorTile = tile; } - + else if (!tile.getLevel().isEmpty()) + { +// this.addTile(dc, tile); +// return; + + // Issue a request for the parent before descending to the children. +// if (tile.getLevelNumber() < this.levels.getNumLevels()) +// { +// // Request only tiles with data associated at this level +// if (!this.levels.isResourceAbsent(tile)) +// this.requestTexture(dc, tile); +// } + } Tile[] subTiles = tile.subdivide(this.levels.getLevel(tile.getLevelNumber() + 1), cache, this); for (Tile child : subTiles) { // Put all sub-tiles in the terrain tile cache to avoid repeatedly allocating them each frame. Sub @@ -458,6 +561,17 @@ protected void addTile(DrawContext dc, GpuTextureTile tile) { return; } + // Level 0 loads may be forced + if (tile.getLevelNumber() == 0 && this.forceLevelZeroLoads && !tile.isTextureInMemory(dc.getTextureCache())) + { + this.forceTextureLoad(tile); + if (tile.isTextureInMemory(dc.getTextureCache())) + { + this.currentTiles.add(tile); + return; + } + } + // The tile's texture is not in memory. Issue a request for the texture data if the tile is not already marked // as an absent resource. We ignore absent resources to avoid flooding the system with requests for resources // that are never resolved. @@ -470,6 +584,9 @@ protected void addTile(DrawContext dc, GpuTextureTile tile) { // application is resumed with the view close to the globe. In that case, the level zero tiles are never // initially loaded and the tile that meets the render criteria may have no data. By issuing a request for // level zero ancestor tiles, we ensure that something displays when the application resumes. + if (this.currentAncestorTile.getLevelNumber() == 0 && this.forceLevelZeroLoads && + !this.currentAncestorTile.isTextureInMemory(dc.getTextureCache())) + this.forceTextureLoad(this.currentAncestorTile); if (this.currentAncestorTile.isTextureInMemory(dc.getGpuResourceCache())) { tile.setFallbackTile(this.currentAncestorTile); @@ -520,289 +637,6 @@ protected Extent computeTileExtent(DrawContext dc, GpuTextureTile tile) { return Sector.computeBoundingBox(dc.getGlobe(), dc.getVerticalExaggeration(), tile.getSector()); } - /** - * Returns the memory cache used to cache texture tiles, initializing the cache if it doesn't yet exist. - * - * @return the memory cache associated with texture tiles. - */ - protected MemoryCache getTextureTileCache() { - if (!WorldWind.getMemoryCacheSet().contains(GpuTextureTile.class.getName())) { - long size = Configuration.getLongValue(AVKey.GPU_TEXTURE_TILE_CACHE_SIZE); - MemoryCache cache = new BasicMemoryCache((long) (0.8 * size), size); - cache.setName("Texture Tiles"); - WorldWind.getMemoryCacheSet().put(GpuTextureTile.class.getName(), cache); - } - - return WorldWind.getMemoryCacheSet().get(GpuTextureTile.class.getName()); - } - - // **************************************************************// - // ********************** Retrieval ***************************// - // **************************************************************// - - protected void requestTile(DrawContext dc, GpuTextureTile tile) { - Runnable task = this.createRequestTask(dc, tile); - if (task == null) { - String msg = Logging.getMessage("nullValue.TaskIsNull"); - Logging.warning(msg); - return; - } - - this.requestQ.add(task); - } - - /** - * Create a task to load a tile. - * - * @param dc - * current draw context. - * @param tile - * tile to load. - * @return new task. - */ - protected Runnable createRequestTask(DrawContext dc, GpuTextureTile tile) { - double priority = this.computeTilePriority(dc, tile); - tile.setPriority(priority); - return new RequestTask(tile, this, priority); - } - - /** - * Compute the priority of loading this tile, based on distance from the eye to the tile's center point. Tiles - * closer to the eye have higher priority than those far from the eye. - * - * @param dc - * current draw context. - * @param tile - * tile for which to compute the priority. - * @return tile priority. A lower number indicates higher priority. - */ - protected double computeTilePriority(DrawContext dc, GpuTextureTile tile) { - // Tile priority is ordered from low (most priority) to high (least priority). Assign the tile priority based - // on square distance form the eye point. Since we don't care about the actual distance this enables us to - // avoid a square root computation. Tiles further from the eye point are loaded last. - return dc.getView().getEyePoint().distanceToSquared3(tile.getExtent().getCenter()); - } - - /** - * Load a tile. If the tile exists in the file cache, it will be loaded from the file cache. If not, it will be - * requested from the network. - * - * @param tile - * tile to load. - */ - protected void loadTile(GpuTextureTile tile) { - URL textureURL = this.getDataFileStore().findFile(tile.getPath(), false); - if (textureURL != null) { - this.loadTileFromCache(tile, textureURL); - } else { - this.retrieveTexture(tile, this.createDownloadPostProcessor(tile)); - } - } - - /** - * Load a tile from the file cache. - * - * @param tile - * tile to load. - * @param textureURL - * local URL to the cached resource. - */ - protected void loadTileFromCache(GpuTextureTile tile, URL textureURL) { - GpuTextureData textureData; - - synchronized (this.fileLock) { - textureData = this.createTextureData(textureURL); - } - - if (textureData != null) { - tile.setTextureData(textureData); - - // The tile's size has changed, so update its size in the memory cache. - MemoryCache cache = this.getTextureTileCache(); - if (cache.contains(tile.getTileKey())) cache.put(tile.getTileKey(), tile); - - // Mark the tile as not absent to ensure that it is used, and cause any World Windows containing this layer - // to repaint themselves. - this.levels.unmarkResourceAbsent(tile); - this.firePropertyChange(AVKey.LAYER, null, this); - } else { - // Assume that something is wrong with the file and delete it. - this.getDataFileStore().removeFile(textureURL); - String message = Logging.getMessage("generic.DeletedCorruptDataFile", textureURL); - Logging.info(message); - } - } - - protected GpuTextureData createTextureData(URL textureURL) { - return GpuTextureData.createTextureData(textureURL); - } - - /** - * Create a post processor for a tile retrieval task. - * - * @param tile - * tile to create a post processor for. - * @return new post processor. - */ - protected DownloadPostProcessor createDownloadPostProcessor(GpuTextureTile tile) { - return new DownloadPostProcessor(tile, this, this.getDataFileStore()); - } - - /** - * Retrieve a tile from the network. This method initiates an asynchronous retrieval task and then returns. - * - * @param tile - * tile to download. - * @param postProcessor - * post processor to handle the retrieval. - */ - protected void retrieveTexture(GpuTextureTile tile, DownloadPostProcessor postProcessor) { - this.retrieveRemoteTexture(tile, postProcessor); - } - - protected void retrieveRemoteTexture(GpuTextureTile tile, DownloadPostProcessor postProcessor) { - if (!this.isNetworkRetrievalEnabled()) { - this.getLevels().markResourceAbsent(tile); - return; - } - - if (!WorldWind.getRetrievalService().isAvailable()) return; - - URL url; - try { - url = tile.getResourceURL(); - } catch (MalformedURLException e) { - Logging.error(Logging.getMessage("layers.TextureLayer.ExceptionCreatingTextureUrl", tile), e); - return; - } - - if (WorldWind.getNetworkStatus().isHostUnavailable(url)) { - this.getLevels().markResourceAbsent(tile); - return; - } - - Retriever retriever = URLRetriever.createRetriever(url, postProcessor); - if (retriever == null) { - Logging.error(Logging.getMessage("layers.TextureLayer.UnknownRetrievalProtocol", url.toString())); - return; - } - retriever.setValue(URLRetriever.EXTRACT_ZIP_ENTRY, "true"); // supports legacy layers - - // Apply any overridden timeouts. - Integer connectTimeout = AVListImpl.getIntegerValue(this, AVKey.URL_CONNECT_TIMEOUT); - if (connectTimeout != null && connectTimeout > 0) retriever.setConnectTimeout(connectTimeout); - - Integer readTimeout = AVListImpl.getIntegerValue(this, AVKey.URL_READ_TIMEOUT); - if (readTimeout != null && readTimeout > 0) retriever.setReadTimeout(readTimeout); - - Integer staleRequestLimit = AVListImpl.getIntegerValue(this, AVKey.RETRIEVAL_QUEUE_STALE_REQUEST_LIMIT); - if (staleRequestLimit != null && staleRequestLimit > 0) retriever.setStaleRequestLimit(staleRequestLimit); - - WorldWind.getRetrievalService().runRetriever(retriever, tile.getPriority()); - } - - protected static class RequestTask implements Runnable, Comparable { - protected GpuTextureTile tile; - protected TiledImageLayer layer; - protected double priority; - - public RequestTask(GpuTextureTile tile, TiledImageLayer layer, double priority) { - if (tile == null) { - String msg = Logging.getMessage("nullValue.TileIsNull"); - Logging.error(msg); - throw new IllegalArgumentException(msg); - } - - if (layer == null) { - String msg = Logging.getMessage("nullValue.LayerIsNull"); - Logging.error(msg); - throw new IllegalArgumentException(msg); - } - - this.tile = tile; - this.layer = layer; - this.priority = priority; - } - - public void run() { - if (Thread.currentThread().isInterrupted()) return; // This task was cancelled because it's a duplicate or for some other reason. - - this.layer.loadTile(this.tile); - } - - public int compareTo(RequestTask that) { - if (that == null) return -1; - - return this.priority < that.priority ? -1 : (this.priority > that.priority ? 1 : 0); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof RequestTask)) return false; - - RequestTask that = (RequestTask) o; - return this.tile.equals(that.tile); - } - - @Override - public int hashCode() { - return this.tile.hashCode(); - } - - @Override - public String toString() { - return this.tile.toString(); - } - } - - protected static class DownloadPostProcessor extends AbstractRetrievalPostProcessor { - protected GpuTextureTile tile; - protected TiledImageLayer layer; - protected FileStore fileStore; - - public DownloadPostProcessor(GpuTextureTile tile, TiledImageLayer layer, FileStore fileStore) { - super(layer); - - this.tile = tile; - this.layer = layer; - this.fileStore = fileStore; - } - - @Override - protected void markResourceAbsent() { - this.layer.getLevels().markResourceAbsent(this.tile); - } - - @Override - protected Object getFileLock() { - return this.layer.fileLock; - } - - @Override - protected File doGetOutputFile() { - return layer.getDataFileStore().newFile(this.tile.getPath()); - } - - @Override - protected ByteBuffer handleSuccessfulRetrieval() { - ByteBuffer buffer = super.handleSuccessfulRetrieval(); - - if (buffer != null) { - // Fire a property change to denote that the layer's backing data has changed. - this.layer.firePropertyChange(AVKey.LAYER, null, this); - } - - return buffer; - } - - @Override - protected ByteBuffer handleTextContent() throws IOException { - this.markResourceAbsent(); - - return super.handleTextContent(); - } - } // **************************************************************// // ******************** Configuration *************************// @@ -964,7 +798,7 @@ public static Element createTiledImageLayerConfigElements(AVList params, Element // Optional behavior properties. WWXML.checkAndAppendBooleanElement(params, AVKey.FORCE_LEVEL_ZERO_LOADS, context, "ForceLevelZeroLoads"); WWXML.checkAndAppendBooleanElement(params, AVKey.RETAIN_LEVEL_ZERO_TILES, context, "RetainLevelZeroTiles"); - // WWXML.checkAndAppendBooleanElement(params, AVKey.USE_MIP_MAPS, context, "UseMipMaps"); + WWXML.checkAndAppendBooleanElement(params, AVKey.USE_MIP_MAPS, context, "UseMipMaps"); WWXML.checkAndAppendBooleanElement(params, AVKey.USE_TRANSPARENT_TEXTURES, context, "UseTransparentTextures"); WWXML.checkAndAppendDoubleElement(params, AVKey.DETAIL_HINT, context, "DetailHint"); @@ -1057,124 +891,24 @@ public static AVList getTiledImageLayerConfigParams(Element domElement, AVList p // Image format properties. WWXML.checkAndSetStringParam(domElement, params, AVKey.IMAGE_FORMAT, "ImageFormat", xpath); WWXML.checkAndSetUniqueStringsParam(domElement, params, AVKey.AVAILABLE_IMAGE_FORMATS, "AvailableImageFormats/ImageFormat", xpath); + WWXML.checkAndSetStringParam(domElement, params, AVKey.TEXTURE_FORMAT, "TextureFormat", xpath); // Optional behavior properties. WWXML.checkAndSetDoubleParam(domElement, params, AVKey.DETAIL_HINT, "DetailHint", xpath); - - // Retrieval properties. Convert the Long time values to Integers. - WWXML.checkAndSetTimeParamAsInteger(domElement, params, AVKey.URL_CONNECT_TIMEOUT, "RetrievalTimeouts/ConnectTimeout/Time", xpath); + WWXML.checkAndSetBooleanParam(domElement, params, AVKey.FORCE_LEVEL_ZERO_LOADS, "ForceLevelZeroLoads", xpath); + WWXML.checkAndSetBooleanParam(domElement, params, AVKey.RETAIN_LEVEL_ZERO_TILES, "RetainLevelZeroTiles", xpath); + WWXML.checkAndSetBooleanParam(domElement, params, AVKey.USE_MIP_MAPS, "UseMipMaps", xpath); + WWXML.checkAndSetBooleanParam(domElement, params, AVKey.USE_TRANSPARENT_TEXTURES, "UseTransparentTextures", xpath); +// WWXML.checkAndSetColorArrayParam(domElement, params, AVKey.TRANSPARENCY_COLORS, "TransparencyColors/Color", xpath); + + // Retrieval properties. Convert the Long time values to Integers. + WWXML.checkAndSetTimeParamAsInteger(domElement, params, AVKey.URL_CONNECT_TIMEOUT, "RetrievalTimeouts/ConnectTimeout/Time", xpath); WWXML.checkAndSetTimeParamAsInteger(domElement, params, AVKey.URL_READ_TIMEOUT, "RetrievalTimeouts/ReadTimeout/Time", xpath); WWXML.checkAndSetTimeParamAsInteger(domElement, params, AVKey.RETRIEVAL_QUEUE_STALE_REQUEST_LIMIT, "RetrievalTimeouts/StaleRequestLimit/Time", xpath); return params; } - // ============== Bulk Download ======================= // - // ============== Bulk Download ======================= // - // ============== Bulk Download ======================= // - - /** - * Start a new {@link BulkRetrievalThread} that downloads all imagery for a given sector and resolution to the - * current World Wind file cache, without downloading imagery that is already in the cache. - *

- * This method creates and starts a thread to perform the download. A reference to the thread is returned. To create a downloader that has not been started, - * construct a {@link TiledImageLayerBulkDownloader}. - *

- * Note that the target resolution must be provided in radians of latitude per texel, which is the resolution in meters divided by the globe radius. - * - * @param sector - * the sector to download imagery for. - * @param resolution - * the target resolution, provided in radians of latitude per texel. - * @param listener - * an optional retrieval listener. May be null. - * @return the {@link BulkRetrievalThread} executing the retrieval or null if the specified sector does - * not intersect the layer bounding sector. - * @throws IllegalArgumentException - * if the sector is null or the resolution is less than zero. - * @see TiledImageLayerBulkDownloader - */ - public BulkRetrievalThread makeLocal(Sector sector, double resolution, BulkRetrievalListener listener) { - return makeLocal(sector, resolution, null, listener); - } - - /** - * Start a new {@link BulkRetrievalThread} that downloads all imagery for a given sector and resolution to a - * specified {@link FileStore}, without downloading imagery that is already in the file store. - *

- * This method creates and starts a thread to perform the download. A reference to the thread is returned. To create a downloader that has not been started, - * construct a {@link TiledImageLayerBulkDownloader}. - *

- * Note that the target resolution must be provided in radians of latitude per texel, which is the resolution in meters divided by the globe radius. - * - * @param sector - * the sector to download data for. - * @param resolution - * the target resolution, provided in radians of latitude per texel. - * @param fileStore - * the file store in which to place the downloaded imagery. If null the current World Wind file - * cache is used. - * @param listener - * an optional retrieval listener. May be null. - * @return the {@link BulkRetrievalThread} executing the retrieval or null if the specified sector does - * not intersect the layer bounding sector. - * @throws IllegalArgumentException - * if the sector is null or the resolution is less than zero. - * @see TiledImageLayerBulkDownloader - */ - public BulkRetrievalThread makeLocal(Sector sector, double resolution, FileStore fileStore, BulkRetrievalListener listener) { - Sector targetSector = sector != null ? getLevels().getSector().intersection(sector) : null; - if (targetSector == null) return null; - - TiledImageLayerBulkDownloader thread = new TiledImageLayerBulkDownloader(this, targetSector, resolution, fileStore != null ? fileStore : this.getDataFileStore(), listener); - thread.setDaemon(true); - thread.start(); - return thread; - } - - /** - * Get the estimated size in bytes of the imagery not in the World Wind file cache for the given sector and - * resolution. - *

- * Note that the target resolution must be provided in radians of latitude per texel, which is the resolution in meters divided by the globe radius. - * - * @param sector - * the sector to estimate. - * @param resolution - * the target resolution, provided in radians of latitude per texel. - * @return the estimated size in bytes of the missing imagery. - * @throws IllegalArgumentException - * if the sector is null or the resolution is less than zero. - */ - public long getEstimatedMissingDataSize(Sector sector, double resolution) { - return this.getEstimatedMissingDataSize(sector, resolution, null); - } - - /** - * Get the estimated size in bytes of the imagery not in a specified file store for a specified sector and - * resolution. - *

- * Note that the target resolution must be provided in radians of latitude per texel, which is the resolution in meters divided by the globe radius. - * - * @param sector - * the sector to estimate. - * @param resolution - * the target resolution, provided in radians of latitude per texel. - * @param fileStore - * the file store to examine. If null the current World Wind file cache is used. - * @return the estimated size in byte of the missing imagery. - * @throws IllegalArgumentException - * if the sector is null or the resolution is less than zero. - */ - public long getEstimatedMissingDataSize(Sector sector, double resolution, FileStore fileStore) { - Sector targetSector = sector != null ? getLevels().getSector().intersection(sector) : null; - if (targetSector == null) return 0; - - TiledImageLayerBulkDownloader downloader = new TiledImageLayerBulkDownloader(this, sector, resolution, fileStore != null ? fileStore : this.getDataFileStore(), null); - - return downloader.getEstimatedMissingDataSize(); - } - protected boolean isTextureFileExpired(GpuTextureTile tile, java.net.URL textureURL, FileStore fileStore) { if (!WWIO.isFileOutOfDate(textureURL, tile.getLevel().getExpiryTime())) return false; diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/layers/TiledImageLayerBulkDownloader.java b/WorldWindAndroid/src/gov/nasa/worldwind/layers/TiledImageLayerBulkDownloader.java index 1fb49c5..d2c88f5 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/layers/TiledImageLayerBulkDownloader.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/layers/TiledImageLayerBulkDownloader.java @@ -34,7 +34,7 @@ public class TiledImageLayerBulkDownloader extends BulkRetrievalThread protected final static int MAX_TILE_COUNT_PER_REGION = 200; protected final static long DEFAULT_AVERAGE_FILE_SIZE = 350000L; - protected final TiledImageLayer layer; + protected final BasicTiledImageLayer layer; protected final int level; protected List missingTiles; @@ -50,7 +50,7 @@ public class TiledImageLayerBulkDownloader extends BulkRetrievalThread * * @throws IllegalArgumentException if either the layer or sector are null, or the resolution is less than zero. */ - public TiledImageLayerBulkDownloader(TiledImageLayer layer, Sector sector, double resolution, + public TiledImageLayerBulkDownloader(BasicTiledImageLayer layer, Sector sector, double resolution, BulkRetrievalListener listener) { // Arguments checked in parent constructor @@ -74,7 +74,7 @@ public TiledImageLayerBulkDownloader(TiledImageLayer layer, Sector sector, doubl * @throws IllegalArgumentException if either the layer, the sector or file store are null, or the resolution is * less than zero. */ - public TiledImageLayerBulkDownloader(TiledImageLayer layer, Sector sector, double resolution, + public TiledImageLayerBulkDownloader(BasicTiledImageLayer layer, Sector sector, double resolution, FileStore fileStore, BulkRetrievalListener listener) { // Arguments checked in parent constructor @@ -163,14 +163,14 @@ protected synchronized void submitMissingTilesRequests() throws InterruptedExcep } } - protected TiledImageLayer.DownloadPostProcessor createBulkDownloadPostProcessor(GpuTextureTile tile) + protected BasicTiledImageLayer.DownloadPostProcessor createBulkDownloadPostProcessor(GpuTextureTile tile) { return new BulkDownloadPostProcessor(tile, this.layer, this.fileStore); } - protected class BulkDownloadPostProcessor extends TiledImageLayer.DownloadPostProcessor + protected class BulkDownloadPostProcessor extends BasicTiledImageLayer.DownloadPostProcessor { - public BulkDownloadPostProcessor(GpuTextureTile tile, TiledImageLayer layer, FileStore fileStore) + public BulkDownloadPostProcessor(GpuTextureTile tile, BasicTiledImageLayer layer, FileStore fileStore) { super(tile, layer, fileStore); } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/layers/WMSTiledImageLayer.java b/WorldWindAndroid/src/gov/nasa/worldwind/layers/WMSTiledImageLayer.java index 861acce..af15550 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/layers/WMSTiledImageLayer.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/layers/WMSTiledImageLayer.java @@ -27,7 +27,7 @@ * @author pabercrombie * @version $Id: WMSTiledImageLayer.java 758 2012-09-06 20:11:41Z tgaskins $ */ -public class WMSTiledImageLayer extends TiledImageLayer { +public class WMSTiledImageLayer extends BasicTiledImageLayer { private static final String[] formatOrderPreference = new String[] { "image/png", "image/jpeg" }; public WMSTiledImageLayer(AVList params) { @@ -110,9 +110,9 @@ public static AVList wmsGetParamsFromCapsDoc(WMSCapabilities caps, AVList params // Setup WMS URL builder. params.setValue(AVKey.WMS_VERSION, caps.getVersion()); - params.setValue(AVKey.TILE_URL_BUILDER, new URLBuilder(params)); // Setup default WMS tiled image layer behaviors. - params.setValue(AVKey.USE_TRANSPARENT_TEXTURES, true); + params.setValue(AVKey.USE_TRANSPARENT_TEXTURES, new Boolean(true)); + params.setValue(AVKey.TILE_URL_BUILDER, new URLBuilder(params)); return params; } @@ -126,6 +126,7 @@ public static class URLBuilder implements TileUrlBuilder { private final String wmsVersion; private final String crs; private final String backgroundColor; + private boolean useTransparency; public String URLTemplate; public URLBuilder(AVList params) { @@ -133,6 +134,8 @@ public URLBuilder(AVList params) { this.styleNames = params.getStringValue(AVKey.STYLE_NAMES); this.imageFormat = params.getStringValue(AVKey.IMAGE_FORMAT); this.backgroundColor = params.getStringValue(AVKey.WMS_BACKGROUND_COLOR); + Boolean b = (Boolean) params.getValue(AVKey.USE_TRANSPARENT_TEXTURES); + if (b != null) this.useTransparency = b; String version = params.getStringValue(AVKey.WMS_VERSION); if (version == null || version.compareTo(MAX_VERSION) >= 0) { @@ -155,8 +158,10 @@ public URL getURL(Tile tile, String altImageFormat) throws MalformedURLException sb.append(this.crs); sb.append("&layers=").append(this.layerNames); sb.append("&styles=").append(this.styleNames != null ? this.styleNames : ""); - sb.append("&transparent=TRUE"); - if (this.backgroundColor != null) sb.append("&bgcolor=").append(this.backgroundColor); + if(this.useTransparency) + sb.append("&transparent=TRUE"); + if (this.backgroundColor != null) + sb.append("&bgcolor=").append(this.backgroundColor); this.URLTemplate = sb.toString(); } else { diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/layers/WorldMapLayer.java b/WorldWindAndroid/src/gov/nasa/worldwind/layers/WorldMapLayer.java index 17bc86c..4b74626 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/layers/WorldMapLayer.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/layers/WorldMapLayer.java @@ -5,9 +5,8 @@ */ package gov.nasa.worldwind.layers; -import gov.nasa.worldwind.Configuration; -import gov.nasa.worldwind.View; -import gov.nasa.worldwind.WorldWindow; +import android.opengl.ETC1Util; +import gov.nasa.worldwind.*; import gov.nasa.worldwind.avlist.AVKey; import gov.nasa.worldwind.cache.GpuResourceCache; import gov.nasa.worldwind.exception.WWRuntimeException; @@ -43,17 +42,17 @@ * location is clicked within the layer's map. Specify WorldMapLayer.class when constructing the ClickAndGoSelectListener. *

* Note: This layer may not be shared among multiple {@link WorldWindow}s. Edited By: Nicola Dorigatti, Trilogis - * + * * @author Nicola Dorigatti * @version $Id: WorldMapLayer.java 1 2011-07-16 23:22:47Z dcollins $ */ public class WorldMapLayer extends AbstractLayer { - protected static final String VERTEX_SHADER_PATH_COLOR = "shaders/WorldMapLayerColor.vert"; - protected static final String FRAGMENT_SHADER_PATH_COLOR = "shaders/WorldMapLayerColor.frag"; - protected static final String VERTEX_SHADER_PATH_TEXTURE = "shaders/WorldMapLayerTexture.vert"; - protected static final String FRAGMENT_SHADER_PATH_TEXTURE = "shaders/WorldMapLayerTexture.frag"; + protected static final int VERTEX_SHADER_PATH_COLOR = R.raw.simple_vert; + protected static final int FRAGMENT_SHADER_PATH_COLOR = R.raw.uniform_color_frag; + protected static final int VERTEX_SHADER_PATH_TEXTURE = R.raw.diffuse_tex_vert; + protected static final int FRAGMENT_SHADER_PATH_TEXTURE = R.raw.etc1alphafrag; - protected String iconFilePath = "images/earth-map-512x256.png";; + protected String iconFilePath; protected double toViewportScale = 0.2; protected double iconScale = 0.5; protected int borderWidth = 20; @@ -72,16 +71,30 @@ public class WorldMapLayer extends AbstractLayer { protected final Object programTextureKey = new Object(); // Draw it as ordered with an eye distance of 0 so that it shows up in front of most other things. protected OrderedIcon orderedImage = new OrderedIcon(); + private Matrix texMatrix = Matrix.fromIdentity(); + + private float[] unitQuadVerts = new float[] { 0, 0, 1, 0, 1, 1, 0, 1 }; + private FloatBuffer vertexBuf = createBuffer(unitQuadVerts); + private float[] textureVerts = new float[] { 0, 1, 1, 1, 1, 0, 0, 0 }; + private FloatBuffer textureBuf = createBuffer(textureVerts); protected class OrderedIcon implements OrderedRenderable { + @Override + public Layer getLayer() { + return WorldMapLayer.this; + } + + @Override public double getDistanceFromEye() { return 0; } + @Override public void pick(DrawContext dc, Point pickPoint) { WorldMapLayer.this.drawIcon(dc); } + @Override public void render(DrawContext dc) { WorldMapLayer.this.drawIcon(dc); } @@ -98,7 +111,7 @@ public WorldMapLayer() { /** * Displays a world map overlay with a current position crosshair in a screen corner - * + * * @param iconFilePath * the world map image path and filename */ @@ -111,10 +124,12 @@ public WorldMapLayer(String iconFilePath) { /** * Returns the layer's current icon file path. - * + * * @return the icon file path */ public String getIconFilePath() { + if(iconFilePath==null || iconFilePath.isEmpty()) + iconFilePath = ETC1Util.isETC1Supported() ? "images/earth-map-512x256.pkm" : "images/earth-map-512x256.png"; return iconFilePath; } @@ -122,7 +137,7 @@ public String getIconFilePath() { * Sets the world map icon's image location. The layer first searches for this location in the current Java * classpath. If not found then the specified path is assumed to refer to the local file system. found there then * the - * + * * @param iconFilePath * the path to the icon's image file */ @@ -137,7 +152,7 @@ public void setIconFilePath(String iconFilePath) { /** * Returns the layer's world map-to-viewport scale factor. - * + * * @return the world map-to-viewport scale factor */ public double getToViewportScale() { @@ -149,7 +164,7 @@ public double getToViewportScale() { * scale factor is used only when the layer's resize behavior is AVKey.RESIZE_STRETCH or AVKey.RESIZE_SHRINK_ONLY. * The icon's width is adjusted to occupy the proportion of the viewport's width indicated by this factor. The * icon's height is adjusted to maintain the world map image's native aspect ratio. - * + * * @param toViewportScale * the world map to viewport scale factor */ @@ -159,7 +174,7 @@ public void setToViewportScale(double toViewportScale) { /** * Returns the icon scale factor. See {@link #setIconScale(double)} for a description of the scale factor. - * + * * @return the current icon scale */ public double getIconScale() { @@ -171,7 +186,7 @@ public double getIconScale() { * in its image file. Values greater than 1 magify the image, values less than one minify it. If the layer's resize * behavior is other than AVKey.RESIZE_KEEP_FIXED_SIZE, the icon's displayed sized is further affected by the value * specified by {@link #setToViewportScale(double)} and the current viewport size. - * + * * @param iconScale * the icon scale factor */ @@ -181,7 +196,7 @@ public void setIconScale(double iconScale) { /** * Returns the world map icon's resize behavior. - * + * * @return the icon's resize behavior */ public String getResizeBehavior() { @@ -197,7 +212,7 @@ public String getResizeBehavior() { * map-to-viewport scale and by the icon's image file size scaled by the current icon scale. If the value is * AVKey.RESIZE_SHRINK_ONLY (the default), icon sizing behaves as for AVKey.RESIZE_STRETCH but the icon will not * grow larger than the size specified in its image file scaled by the current icon scale. - * + * * @param resizeBehavior * the desired resize behavior */ @@ -211,7 +226,7 @@ public int getBorderWidth() { /** * Sets the world map icon offset from the viewport border. - * + * * @param borderWidth * the number of pixels to offset the world map icon from the borders indicated by {@link #setPosition(String)}. */ @@ -221,7 +236,7 @@ public void setBorderWidth(int borderWidth) { /** * Returns the current relative world map icon position. - * + * * @return the current world map position */ public String getPosition() { @@ -231,7 +246,7 @@ public String getPosition() { /** * Sets the relative viewport location to display the world map icon. Can be one of AVKey.NORTHEAST, AVKey.NORTHWEST * (the default), AVKey.SOUTHEAST, or SOUTHWEST. These indicate the corner of the viewport to place the icon. - * + * * @param position * the desired world map position */ @@ -246,7 +261,7 @@ public void setPosition(String position) { /** * Returns the current worldmap image location. - * + * * @return the current location center. May be null. */ public Vec4 getLocationCenter() { @@ -259,7 +274,7 @@ public Vec4 getLocationCenter() { * is the window's lower left corner. Positive X values are to the right of the origin, positive Y values are * upwards from the origin. The final image location will be affected by the currently specified location offset if * a non-null location offset has been specified (see #setLocationOffset). - * + * * @param locationCenter * the location center. May be null. * @see #locationCenter the screen location at which to place the map. @@ -270,7 +285,7 @@ public void setLocationCenter(Vec4 locationCenter) { /** * Returns the current location offset. See #setLocationOffset for a description of the offset and its values. - * + * * @return the location offset. Will be null if no offset has been specified. */ public Vec4 getLocationOffset() { @@ -279,7 +294,7 @@ public Vec4 getLocationOffset() { /** * Specifies a placement offset from the worldmap's position on the screen. - * + * * @param locationOffset * the number of pixels to shift the worldmap image from its specified screen position. A * positive X value shifts the image to the right. A positive Y value shifts the image up. If @@ -315,7 +330,7 @@ public void setShowFootprint(boolean state) { /** * Get the current view footprint position list. May be null if no footprint is displayed or none has been * computed. - * + * * @return the current view footprint position list - may be null. */ public List getFootPrintPositions() { @@ -352,6 +367,7 @@ protected void drawIcon(DrawContext dc) { } GLES20.glDisable(GLES20.GL_DEPTH_TEST); + WorldWindowImpl.glCheckError("glDisable"); // iconWidth = 512; double width = this.getScaledIconWidth(); @@ -377,24 +393,39 @@ protected void drawIcon(DrawContext dc) { if (!dc.isPickingMode()) { GLES20.glEnable(GLES20.GL_BLEND); + WorldWindowImpl.glCheckError("glEnable:GL_BLEND"); + GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); + WorldWindowImpl.glCheckError("glBlendFunc"); + GpuProgram colorProgram = this.getGpuProgram(dc.getGpuResourceCache(), programColorKey, VERTEX_SHADER_PATH_COLOR, FRAGMENT_SHADER_PATH_COLOR); // Draw background color behind the map if (colorProgram != null) { colorProgram.bind(); colorProgram.loadUniformMatrix("mvpMatrix", mvp); - colorProgram.loadUniform4f("uColor", backColor[0], backColor[1], backColor[2], backColor[3] * this.getOpacity()); + colorProgram.loadUniform1f("uOpacity", this.getOpacity()); + colorProgram.loadUniform4f("uColor", backColor[0], backColor[1], backColor[2], backColor[3]); float[] unitQuadVerts = new float[] { 0, 0, 1, 0, 1, 1, 0, 1 }; int pointLocation = colorProgram.getAttribLocation("vertexPoint"); GLES20.glEnableVertexAttribArray(pointLocation); + + WorldWindowImpl.glCheckError("glEnableVertexAttribArray"); FloatBuffer vertexBuf = ByteBuffer.allocateDirect(unitQuadVerts.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); vertexBuf.put(unitQuadVerts); vertexBuf.rewind(); GLES20.glVertexAttribPointer(pointLocation, 2, GLES20.GL_FLOAT, false, 0, vertexBuf); + + WorldWindowImpl.glCheckError("glVertexAttribPointer"); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, unitQuadVerts.length / 2); + + WorldWindowImpl.glCheckError("glDrawArrays"); GLES20.glDisableVertexAttribArray(pointLocation); + + WorldWindowImpl.glCheckError("glDisableVertexAttribArray"); GLES20.glUseProgram(0); + + WorldWindowImpl.glCheckError("glUseProgram"); } // Draw world map icon @@ -402,30 +433,37 @@ protected void drawIcon(DrawContext dc) { if (textureProgram != null) { textureProgram.bind(); textureProgram.loadUniformMatrix("mvpMatrix", mvp); - GLES20.glEnable(GLES20.GL_TEXTURE_2D); - GLES20.glActiveTexture(GLES20.GL_TEXTURE0); iconTexture.bind(); textureProgram.loadUniformSampler("sTexture", 0); + textureProgram.loadUniformSampler("aTexture", 1); + textureProgram.loadUniform1f("uOpacity", this.getOpacity()); + textureProgram.loadUniformMatrix("texMatrix", texMatrix); - float[] unitQuadVerts = new float[] { 0, 0, 1, 0, 1, 1, 0, 1 }; int pointLocation = textureProgram.getAttribLocation("vertexPoint"); GLES20.glEnableVertexAttribArray(pointLocation); - FloatBuffer vertexBuf = ByteBuffer.allocateDirect(unitQuadVerts.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); - vertexBuf.put(unitQuadVerts); - vertexBuf.rewind(); - GLES20.glVertexAttribPointer(pointLocation, 2, GLES20.GL_FLOAT, false, 0, vertexBuf); - float[] textureVerts = new float[] { 0, 1, 1, 1, 1, 0, 0, 0 }; + WorldWindowImpl.glCheckError("glEnableVertexAttribArray"); + + GLES20.glVertexAttribPointer(pointLocation, 2, GLES20.GL_FLOAT, false, 0, vertexBuf.rewind()); + WorldWindowImpl.glCheckError("glVertexAttribPointer"); + int textureLocation = textureProgram.getAttribLocation("aTextureCoord"); GLES20.glEnableVertexAttribArray(textureLocation); - FloatBuffer textureBuf = ByteBuffer.allocateDirect(textureVerts.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); - textureBuf.put(textureVerts); - textureBuf.rewind(); - GLES20.glVertexAttribPointer(textureLocation, 2, GLES20.GL_FLOAT, false, 0, textureBuf); + WorldWindowImpl.glCheckError("glEnableVertexAttribArray"); + + GLES20.glVertexAttribPointer(textureLocation, 2, GLES20.GL_FLOAT, false, 0, textureBuf.rewind()); + WorldWindowImpl.glCheckError("glVertexAttribPointer"); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, unitQuadVerts.length / 2); + WorldWindowImpl.glCheckError("glDrawArrays"); + GLES20.glDisableVertexAttribArray(pointLocation); + WorldWindowImpl.glCheckError("glDisableVertexAttribArray"); + GLES20.glDisableVertexAttribArray(textureLocation); + WorldWindowImpl.glCheckError("glDisableVertexAttribArray"); + GLES20.glUseProgram(0); - GLES20.glDisable(GLES20.GL_TEXTURE_2D); + WorldWindowImpl.glCheckError("glUseProgram"); } // Draw crosshair for current location modelview = Matrix.fromIdentity(); @@ -437,7 +475,8 @@ protected void drawIcon(DrawContext dc) { colorProgram.bind(); colorProgram.loadUniformMatrix("mvpMatrix", mvp); // Set color - colorProgram.loadUniform4f("uColor", color[0], color[1], color[2], this.getOpacity()); + colorProgram.loadUniform1f("uOpacity", this.getOpacity()); + colorProgram.loadUniform4f("uColor", color[0], color[1], color[2], 1); // Draw crosshair Position groundPos = this.computeGroundPosition(dc, dc.getView()); if (groundPos != null) { @@ -447,15 +486,27 @@ protected void drawIcon(DrawContext dc) { // Draw int pointLocation = colorProgram.getAttribLocation("vertexPoint"); GLES20.glEnableVertexAttribArray(pointLocation); + + WorldWindowImpl.glCheckError("glEnableVertexAttribArray"); float[] verts = new float[] { x - w, y, 0, x + w + 1, y, 0 }; FloatBuffer vertBuf = createBuffer(verts); GLES20.glVertexAttribPointer(pointLocation, 3, GLES20.GL_FLOAT, false, 0, vertBuf); + + WorldWindowImpl.glCheckError("glVertexAttribPointer"); GLES20.glDrawArrays(GLES20.GL_LINE_STRIP, 0, verts.length / 3); + + WorldWindowImpl.glCheckError("glDrawArrays"); verts = new float[] { x, y - w, 0, x, y + w + 1, 0 }; vertBuf = createBuffer(verts); GLES20.glVertexAttribPointer(pointLocation, 3, GLES20.GL_FLOAT, false, 0, vertBuf); + + WorldWindowImpl.glCheckError("glVertexAttribPointer"); GLES20.glDrawArrays(GLES20.GL_LINE_STRIP, 0, verts.length / 3); + + WorldWindowImpl.glCheckError("glDrawArrays"); GLES20.glDisableVertexAttribArray(pointLocation); + + WorldWindowImpl.glCheckError("glDisableVertexAttribArray"); } } // Draw view footprint in map icon space @@ -488,24 +539,40 @@ protected void drawIcon(DrawContext dc) { } int pointLocation = colorProgram.getAttribLocation("vertexPoint"); GLES20.glEnableVertexAttribArray(pointLocation); + + WorldWindowImpl.glCheckError("glEnableVertexAttribArray"); for (ArrayList lineStrip : lineStrips) { float[] verts = convertToArray(lineStrip); FloatBuffer vertBuf = createBuffer(verts); GLES20.glVertexAttribPointer(pointLocation, 3, GLES20.GL_FLOAT, false, 0, vertBuf); + + WorldWindowImpl.glCheckError("glVertexAttribPointer"); GLES20.glDrawArrays(GLES20.GL_LINE_STRIP, 0, verts.length / 3); + + WorldWindowImpl.glCheckError("glDrawArrays"); } GLES20.glDisableVertexAttribArray(pointLocation); + + WorldWindowImpl.glCheckError("glDisableVertexAttribArray"); } } // Draw 1px border around and inside the map if (colorProgram != null) { int pointLocation = colorProgram.getAttribLocation("vertexPoint"); GLES20.glEnableVertexAttribArray(pointLocation); + + WorldWindowImpl.glCheckError("glEnableVertexAttribArray"); float[] vertices = new float[] { 0, 0, 0, (float) width, 0, 0, (float) width, (float) (height - 1), 0, 0, (float) (height - 1), 0, 0, 0, 0 }; FloatBuffer vertBuf = createBuffer(vertices); GLES20.glVertexAttribPointer(pointLocation, 3, GLES20.GL_FLOAT, false, 0, vertBuf); + + WorldWindowImpl.glCheckError("glVertexAttribPointer"); GLES20.glDrawArrays(GLES20.GL_LINE_STRIP, 0, vertices.length / 3); + + WorldWindowImpl.glCheckError("glDrawArrays"); GLES20.glDisableVertexAttribArray(pointLocation); + + WorldWindowImpl.glCheckError("glDisableVertexAttribArray"); } } else { // Picking TODO Copied from compass layer and not tested @@ -517,16 +584,25 @@ protected void drawIcon(DrawContext dc) { this.pickSupport.addPickableObject(colorCode, this, pickPosition, false); GpuProgram textureProgram = this.getGpuProgram(dc.getGpuResourceCache(), programTextureKey, VERTEX_SHADER_PATH_TEXTURE, FRAGMENT_SHADER_PATH_TEXTURE); textureProgram.bind(); - float[] unitQuadVerts = new float[] { 0, 0, 1, 0, 1, 1, 0, 1 }; - FloatBuffer vertexBuf = ByteBuffer.allocateDirect(unitQuadVerts.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); - vertexBuf.put(unitQuadVerts); - vertexBuf.rewind(); + textureProgram.loadUniform1f("uOpacity", 1); + textureProgram.loadUniformMatrix("texMatrix", texMatrix); + int pointLocation = textureProgram.getAttribLocation("vertexPoint"); GLES20.glEnableVertexAttribArray(pointLocation); - GLES20.glVertexAttribPointer(pointLocation, 2, GLES20.GL_FLOAT, false, 0, vertexBuf); + + WorldWindowImpl.glCheckError("glEnableVertexAttribArray"); + GLES20.glVertexAttribPointer(pointLocation, 2, GLES20.GL_FLOAT, false, 0, vertexBuf.rewind()); + + WorldWindowImpl.glCheckError("glVertexAttribPointer"); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, unitQuadVerts.length / 2); + + WorldWindowImpl.glCheckError("glDrawArrays"); GLES20.glDisableVertexAttribArray(pointLocation); + + WorldWindowImpl.glCheckError("glDisableVertexAttribArray"); GLES20.glUseProgram(0); + + WorldWindowImpl.glCheckError("glUseProgram"); this.pickSupport.endPicking(dc); this.pickSupport.resolvePick(dc, dc.getPickPoint(), this); } @@ -535,6 +611,8 @@ protected void drawIcon(DrawContext dc) { Logging.error("Exception drawing WorldMapLayer: " + t.getMessage()); } finally { GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA); + + WorldWindowImpl.glCheckError("glBlendFunc"); } } @@ -549,13 +627,11 @@ private float[] convertToArray(ArrayList lineStrip) { } protected FloatBuffer createBuffer(float[] array) { - FloatBuffer retval = ByteBuffer.allocateDirect(array.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); - retval.put(array); - retval.rewind(); - return retval; + return (FloatBuffer) ByteBuffer.allocateDirect(array.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer() + .put(array).rewind(); } - protected GpuProgram getGpuProgram(GpuResourceCache cache, Object programKey, String shaderPath, String fragmentPath) { + protected GpuProgram getGpuProgram(GpuResourceCache cache, Object programKey, int shaderPath, int fragmentPath) { GpuProgram program = cache.getProgram(programKey); if (program == null) { @@ -643,7 +719,8 @@ protected void initializeTexture(DrawContext dc) { iconStream = new FileInputStream(iconFile); } } - iconTexture = GpuTexture.createTexture(dc, GpuTextureData.createTextureData(iconStream));// TextureIO.newTexture(iconStream, false, null); + String imageMimeType = ETC1Util.isETC1Supported() ? "image/pkm" : "image/png"; + iconTexture = GpuTexture.createTexture(dc, GpuTextureData.createTextureData(iconStream, iconFilePath, imageMimeType, false)); iconTexture.bind(); this.iconWidth = iconTexture.getWidth(); this.iconHeight = iconTexture.getHeight(); @@ -658,7 +735,7 @@ protected void initializeTexture(DrawContext dc) { /** * Compute the lat/lon position of the view center - * + * * @param dc * the current DrawContext * @param view @@ -679,7 +756,7 @@ protected Position computeGroundPosition(DrawContext dc, View view) { /** * Computes the lat/lon of the pickPoint over the world map - * + * * @param dc * the current DrawContext * @param locationSW @@ -707,7 +784,7 @@ protected Position computePickPosition(DrawContext dc, Vec4 locationSW, Rect map /** * Compute the view range footprint on the globe. - * + * * @param dc * the current DrawContext * @param steps @@ -716,7 +793,7 @@ protected Position computePickPosition(DrawContext dc, Vec4 locationSW, Rect map */ protected ArrayList computeViewFootPrint(DrawContext dc, int steps) { ArrayList positions = new ArrayList(); - Position eyePos = dc.getView().getEyePosition(dc.getGlobe()); + Position eyePos = dc.getView().getEyePosition(); Angle distance = Angle.fromRadians(Math.asin(dc.getView().getFarClipDistance() / (dc.getGlobe().getRadius() + eyePos.elevation))); if (distance.degrees > 10) { double headStep = 360d / steps; diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/pick/DepthBufferSupport.java b/WorldWindAndroid/src/gov/nasa/worldwind/pick/DepthBufferSupport.java new file mode 100644 index 0000000..5ddac43 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/pick/DepthBufferSupport.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.pick; + +import android.graphics.Color; +import android.graphics.Point; +import android.opengl.GLES20; +import gov.nasa.worldwind.R; +import gov.nasa.worldwind.cache.GpuResourceCache; +import gov.nasa.worldwind.geom.Vec4; +import gov.nasa.worldwind.render.DrawContext; +import gov.nasa.worldwind.render.GpuProgram; +import gov.nasa.worldwind.terrain.SectorGeometry; +import gov.nasa.worldwind.terrain.SectorGeometryList; +import gov.nasa.worldwind.util.Logging; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Creates color buffer used for depth values. + * + * @author Marek Kedzierski + */ +public class DepthBufferSupport { + + private int mFrameBufferHandle = -1; + private int mDepthBufferHandle = -1; + private boolean mIsInitialized = false; + private int mTextureId = -1; + private int mViewportWidth = -1; + private int mViewportHeight = -1; + private int mTextureWidth; + private int mTextureHeight; + private ByteBuffer mBuffer; + + private boolean programCreationFailed; + private Object programKey; + + public void initialize() { + mTextureWidth=mTextureHeight= Math.max(mViewportWidth, mViewportHeight); + genTexture(); + genBuffers(); + mIsInitialized = true; + } + + public void setup(int width, int height) { + if(mViewportWidth==width && mViewportHeight==height) + return; + mViewportWidth=width; + mViewportHeight=height; + if (mIsInitialized) { + destroy(); + } + initialize(); + } + + public Vec4 getPosition(DrawContext dc, Point pickPoint) { + int pixelIndex = (mViewportHeight-pickPoint.y)*mTextureWidth + pickPoint.x; + mBuffer.position(pixelIndex*4); + Vec4 bufferVal = new Vec4(mBuffer.get() & 0xFF, mBuffer.get() & 0xFF, mBuffer.get() & 0xFF, mBuffer.get() & 0xFF); + Vec4 readPixelsVal = getPositionReadPixels(pickPoint.x, pickPoint.y); + if(!bufferVal.equals(readPixelsVal)) + Logging.warning("Depth Buffer Position mismatch!"); + return bufferVal; + } + + public Vec4 getPositionReadPixels(int x, int y) { + final ByteBuffer pixelBuffer = ByteBuffer.allocateDirect(4).order(ByteOrder.nativeOrder()); + + GLES20.glReadPixels(x, mViewportHeight - y, 1, 1, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, + pixelBuffer); + pixelBuffer.rewind(); + + return new Vec4(mBuffer.get() & 0xFF, mBuffer.get() & 0xFF, mBuffer.get() & 0xFF, mBuffer.get() & 0xFF); + } + + protected GpuProgram getGpuProgram(GpuResourceCache cache) + { + if (this.programCreationFailed) + return null; + + GpuProgram program = cache.getProgram(this.programKey); + + if (program == null) + { + try + { + GpuProgram.GpuProgramSource source = GpuProgram.readProgramSource(R.raw.simple_vert, R.raw.depth_frag); + program = new GpuProgram(source); + cache.put(this.programKey, program); + } + catch (Exception e) + { + String msg = Logging.getMessage("GL.ExceptionLoadingProgram", R.raw.simple_vert, R.raw.depth_frag); + Logging.error(msg); + this.programCreationFailed = true; + } + } + + return program; + } + + public void begin(DrawContext dc) { + if (!mIsInitialized) + initialize(); + + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBufferHandle); + GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, + GLES20.GL_TEXTURE_2D, mTextureId, 0); + int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER); + if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) { + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); + Logging.debug("Could not bind FrameBuffer for color picking." + mTextureId); + } + GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT, + GLES20.GL_RENDERBUFFER, mDepthBufferHandle); + } + + public void draw(DrawContext dc) { + begin(dc); + + SectorGeometryList sgList = dc.getSurfaceGeometry(); + if (sgList == null) + { + Logging.warning(Logging.getMessage("generic.NoSurfaceGeometry")); + return; + } + + GpuProgram program = this.getGpuProgram(dc.getGpuResourceCache()); + if (program == null) + return; // Exception logged in loadGpuProgram. + program.bind(); + dc.setCurrentProgram(program); + + sgList.beginRendering(dc); + try + { + for (SectorGeometry sg : sgList) + { + sg.beginRendering(dc); + try + { + sg.render(dc); + } + finally + { + sg.endRendering(dc); + } + } + } + finally + { + sgList.endRendering(dc); + } + + end(dc); + } + + public void end(DrawContext dc) { + GLES20.glReadPixels(0, 0, mTextureWidth, mTextureHeight, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, + mBuffer.rewind()); + mBuffer.rewind(); + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); + GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, 0); + } + + private void destroy() { + GLES20.glDeleteTextures(1, new int[] { mTextureId }, 0); + GLES20.glDeleteRenderbuffers(1, new int[] { mDepthBufferHandle } , 0); + GLES20.glDeleteFramebuffers(1, new int[] { mFrameBufferHandle }, 0); + } + + private void genTexture() { + int[] textures = new int[1]; + GLES20.glGenTextures(1, textures, 0); + int textureId = textures[0]; + + if (textureId > 0) { + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT); + GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, mTextureWidth, mTextureHeight, 0, + GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null); + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); + mTextureId = textureId; + } + } + + public void genBuffers() { + final int[] frameBuffers = new int[1]; + GLES20.glGenFramebuffers(1, frameBuffers, 0); + mFrameBufferHandle = frameBuffers[0]; + + final int[] depthBuffers = new int[1]; + GLES20.glGenRenderbuffers(1, depthBuffers, 0); + mDepthBufferHandle = depthBuffers[0]; + + GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, mDepthBufferHandle); + GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER, GLES20.GL_DEPTH_COMPONENT16, + mTextureWidth, mTextureHeight); + GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, 0); + + mBuffer = ByteBuffer.allocateDirect(4*mTextureWidth*mTextureHeight).order(ByteOrder.nativeOrder()); + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/pick/PickSupport.java b/WorldWindAndroid/src/gov/nasa/worldwind/pick/PickSupport.java index 776e72d..7ed7b51 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/pick/PickSupport.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/pick/PickSupport.java @@ -4,24 +4,54 @@ */ package gov.nasa.worldwind.pick; +import android.graphics.Color; +import android.graphics.Point; +import android.opengl.GLES20; +import gov.nasa.worldwind.WorldWindowImpl; import gov.nasa.worldwind.geom.Position; import gov.nasa.worldwind.layers.Layer; import gov.nasa.worldwind.render.DrawContext; +import gov.nasa.worldwind.util.Logging; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.HashMap; import java.util.Map; -import android.graphics.Point; -import android.opengl.GLES20; /** * Edited By: Nicola Dorigatti, Trilogis - * + * * @author tag * @version $Id: PickSupport.java 805 2012-09-26 01:47:35Z dcollins $ */ public class PickSupport { protected Map pickableObjects = new HashMap(); - public PickSupport() { + private int mFrameBufferHandle = -1; + private int mDepthBufferHandle = -1; + private boolean mIsInitialized = false; + private int mTextureId = -1; + private int mViewportWidth = -1; + private int mViewportHeight = -1; + private int mTextureWidth; + private int mTextureHeight; + + public void initialize() { + mTextureWidth=mTextureHeight= Math.max(mViewportWidth, mViewportHeight); + genTexture(); + genBuffers(); + mIsInitialized = true; + } + + public void setup(int width, int height) { + if(mViewportWidth==width && mViewportHeight==height) + return; + mViewportWidth=width; + mViewportHeight=height; + if (mIsInitialized) { + destroy(); + } + initialize(); } public void addPickableObject(PickedObject po) { @@ -53,7 +83,7 @@ public PickedObject getTopObject(DrawContext dc, Point pickPoint) { int colorCode = dc.getPickColor(pickPoint); if (colorCode == 0) // getPickColor returns 0 if the pick point selects the clear color. - return null; + return null; PickedObject pickedObject = getPickableObjects().get(colorCode); if (pickedObject == null) return null; @@ -74,27 +104,126 @@ public PickedObject resolvePick(DrawContext dc, Point pickPoint, Layer layer) { return pickedObject; } - // ARE THESE NECESSARY??? - public void beginPicking(DrawContext dc) { + public int getPickColor(int x, int y) { + final ByteBuffer pixelBuffer = ByteBuffer.allocateDirect(4).order(ByteOrder.nativeOrder()); + + GLES20.glReadPixels(x, mViewportHeight - y, 1, 1, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, + pixelBuffer); + pixelBuffer.rewind(); - // gl.glPushAttrib(GL.GL_ENABLE_BIT | GL.GL_CURRENT_BIT); + final int r = pixelBuffer.get(0) & 0xff; + final int g = pixelBuffer.get(1) & 0xff; + final int b = pixelBuffer.get(2) & 0xff; + final int a = pixelBuffer.get(3) & 0xff; + return Color.argb(a, r, g, b); + } + + public void bindBuffer(DrawContext dc) { + dc.setPickingMode(true); + GLES20.glDisable(GLES20.GL_DITHER); + WorldWindowImpl.glCheckError("glDisable: GL_DITHER"); + + GLES20.glDisable(GLES20.GL_BLEND); + WorldWindowImpl.glCheckError("glDisable: GL_BLEND"); + if (dc.isDeepPickingEnabled()) { + GLES20.glDisable(GLES20.GL_DEPTH_TEST); + WorldWindowImpl.glCheckError("glDisable: GL_DEPTH_TEST"); + GLES20.glDepthMask(false); + WorldWindowImpl.glCheckError("glDepthMask(false)"); + } + bindFrameBuffer(); + } + + + public void beginPicking(DrawContext dc) { GLES20.glDisable(GLES20.GL_DITHER); - // GLES20.glDisable(GLES20.GL_GL_LIGHTING); - // GLES20.glDisable(GLES20.GL_FOG); + WorldWindowImpl.glCheckError("glDisable: GL_DITHER"); + GLES20.glDisable(GLES20.GL_BLEND); - GLES20.glDisable(GLES20.GL_TEXTURE_2D); + WorldWindowImpl.glCheckError("glDisable: GL_BLEND"); - if (dc.isDeepPickingEnabled()) GLES20.glDisable(GLES20.GL_DEPTH_TEST); + if (dc.isDeepPickingEnabled()) { + GLES20.glDisable(GLES20.GL_DEPTH_TEST); + WorldWindowImpl.glCheckError("glDisable: GL_DEPTH_TEST"); + GLES20.glDepthMask(false); + WorldWindowImpl.glCheckError("glDepthMask(false)"); + } } public void endPicking(DrawContext dc) { - // dc.restoreDefaultBlending(); GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ZERO); + WorldWindowImpl.glCheckError("glBlendFunc"); + GLES20.glDisable(GLES20.GL_BLEND); - // dc.restoreDefaultDepthTesting(); + WorldWindowImpl.glCheckError("glDisable: GL_BLEND"); + GLES20.glEnable(GLES20.GL_DEPTH_TEST); + WorldWindowImpl.glCheckError("glEnable: GL_DEPTH_TEST"); + GLES20.glDepthMask(true); - // ??dc.restoreDefaultCurrentColor(); + WorldWindowImpl.glCheckError("glDepthMask(true)"); + } + + private void genTexture() { + int[] textures = new int[1]; + GLES20.glGenTextures(1, textures, 0); + int textureId = textures[0]; + + if (textureId > 0) { + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT); + GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, mTextureWidth, mTextureHeight, 0, + GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null); + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); + mTextureId = textureId; + } + } + + private void destroy() { + GLES20.glDeleteTextures(1, new int[] { mTextureId }, 0); + GLES20.glDeleteRenderbuffers(1, new int[]{mDepthBufferHandle}, 0); + GLES20.glDeleteFramebuffers(1, new int[]{mFrameBufferHandle}, 0); + } + + public void genBuffers() { + final int[] frameBuffers = new int[1]; + GLES20.glGenFramebuffers(1, frameBuffers, 0); + mFrameBufferHandle = frameBuffers[0]; + + final int[] depthBuffers = new int[1]; + GLES20.glGenRenderbuffers(1, depthBuffers, 0); + mDepthBufferHandle = depthBuffers[0]; + + GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, mDepthBufferHandle); + GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER, GLES20.GL_DEPTH_COMPONENT16, + mTextureWidth, mTextureHeight); + GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, 0); + } + + public void bindFrameBuffer() { + if (!mIsInitialized) + { + initialize(); + } + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBufferHandle); + GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, + GLES20.GL_TEXTURE_2D, mTextureId, 0); + int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER); + if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) { + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); + Logging.debug("Could not bind FrameBuffer for color picking." + mTextureId); + } + GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT, + GLES20.GL_RENDERBUFFER, mDepthBufferHandle); + } + + public void unbindFrameBuffer() { + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); + GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, 0); } } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/render/AbstractShape.java b/WorldWindAndroid/src/gov/nasa/worldwind/render/AbstractShape.java index d6fa4c6..bfc4013 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/render/AbstractShape.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/render/AbstractShape.java @@ -4,28 +4,24 @@ */ package gov.nasa.worldwind.render; +import android.graphics.Point; +import android.opengl.GLES20; import gov.nasa.worldwind.Movable; +import gov.nasa.worldwind.R; import gov.nasa.worldwind.WWObjectImpl; +import gov.nasa.worldwind.WorldWindowImpl; import gov.nasa.worldwind.avlist.AVKey; import gov.nasa.worldwind.cache.GpuResourceCache; import gov.nasa.worldwind.cache.ShapeDataCache; -import gov.nasa.worldwind.geom.Extent; -import gov.nasa.worldwind.geom.Intersection; -import gov.nasa.worldwind.geom.LatLon; -import gov.nasa.worldwind.geom.Line; -import gov.nasa.worldwind.geom.Matrix; -import gov.nasa.worldwind.geom.Position; -import gov.nasa.worldwind.geom.Sector; -import gov.nasa.worldwind.geom.Vec4; +import gov.nasa.worldwind.geom.*; import gov.nasa.worldwind.globes.Globe; import gov.nasa.worldwind.layers.Layer; import gov.nasa.worldwind.pick.PickSupport; import gov.nasa.worldwind.pick.PickedObject; import gov.nasa.worldwind.terrain.Terrain; import gov.nasa.worldwind.util.Logging; + import java.util.List; -import android.graphics.Point; -import android.opengl.GLES20; /** * Provides a base class form several geometric {@link gov.nasa.worldwind.render.Renderable}s. Implements common @@ -34,7 +30,7 @@ * In order to support simultaneous use of this shape with multiple globes (windows), this shape maintains a cache of data computed relative to each globe. * During rendering, the data for the currently active globe, as indicated in the draw context, is made current. Subsequently called methods rely on the * existence of this current data cache entry. - * + * * @author tag * @version $Id: AbstractShape.java 844 2012-10-11 00:35:07Z tgaskins $ */ @@ -50,9 +46,9 @@ public abstract class AbstractShape extends WWObjectImpl implements OrderedRende /** The default geometry regeneration interval. */ protected static final int DEFAULT_GEOMETRY_GENERATION_INTERVAL = 3000; /** The default vertex shader path. This specifies the location of a file within the World Wind archive. */ - protected static final String DEFAULT_VERTEX_SHADER_PATH = "shaders/AbstractShape.vert"; + protected static final int DEFAULT_VERTEX_SHADER_PATH = R.raw.simple_vert; /** The default fragment shader path. This specifies the location of a file within the World Wind archive. */ - protected static final String DEFAULT_FRAGMENT_SHADER_PATH = "shaders/AbstractShape.frag"; + protected static final int DEFAULT_FRAGMENT_SHADER_PATH = R.raw.uniform_color_frag; /** The attributes used if attributes are not specified. */ protected static ShapeAttributes defaultAttributes; @@ -68,7 +64,7 @@ public abstract class AbstractShape extends WWObjectImpl implements OrderedRende * Compute the intersections of a specified line with this shape. If the shape's altitude mode is other than {@link AVKey#ABSOLUTE}, the shape's geometry is * created relative to the specified terrain rather than the terrain used * during rendering, which may be at lower level of detail than required for accurate intersection determination. - * + * * @param line * the line to intersect. * @param terrain @@ -92,7 +88,7 @@ public abstract class AbstractShape extends WWObjectImpl implements OrderedRende * shape on the draw context's ordered renderable list for subsequent rendering. This method is called during {@link #pick(DrawContext, Point)} and * {@link #render(DrawContext)} when it's been determined that the shape is likely to * be visible. - * + * * @param dc * the current draw context. * @return true if the ordered renderable state was successfully computed, otherwise false, in which case the @@ -106,7 +102,7 @@ public abstract class AbstractShape extends WWObjectImpl implements OrderedRende /** * Determines whether this shape's ordered renderable state is valid and can be rendered. Called by {@link #makeOrderedRenderable(DrawContext)}just prior to * adding the shape to the ordered renderable list. - * + * * @param dc * the current draw context. * @return true if this shape is ready to be rendered as an ordered renderable. @@ -119,7 +115,7 @@ public abstract class AbstractShape extends WWObjectImpl implements OrderedRende * attributes. Subclasses should execute the drawing commands specific to the type of shape. *

* A {@link gov.nasa.worldwind.render.AbstractShape.AbstractShapeData} must be current when this method is called. - * + * * @param dc * the current draw context. */ @@ -131,7 +127,7 @@ public abstract class AbstractShape extends WWObjectImpl implements OrderedRende * interior attributes. Subclasses should execute the drawing commands specific to the type of shape. *

* A {@link gov.nasa.worldwind.render.AbstractShape.AbstractShapeData} must be current when this method is called. - * + * * @param dc * the current draw context. */ @@ -141,7 +137,7 @@ public abstract class AbstractShape extends WWObjectImpl implements OrderedRende * Fill this shape's vertex buffer objects. If the vertex buffer object resource IDs don't yet exist, create them. *

* A {@link gov.nasa.worldwind.render.AbstractShape.AbstractShapeData} must be current when this method is called. - * + * * @param dc * the current draw context. */ @@ -149,7 +145,7 @@ public abstract class AbstractShape extends WWObjectImpl implements OrderedRende /** * Creates and returns a new cache entry specific to the subclass. - * + * * @param dc * the current draw context. * @return a data cache entry for the state in the specified draw context. @@ -158,7 +154,7 @@ public abstract class AbstractShape extends WWObjectImpl implements OrderedRende /** * Returns the shape's geographic extent. - * + * * @return the shape's geographic extent. */ protected abstract Sector getSector(); @@ -194,6 +190,7 @@ public abstract class AbstractShape extends WWObjectImpl implements OrderedRende protected Color currentColor = new Color(); protected PickSupport pickSupport = new PickSupport(); protected Layer pickLayer; + protected Layer layer; /** Holds globe-dependent computed data. One entry per globe encountered during {@link #render(DrawContext)}. */ protected ShapeDataCache shapeDataCache = new ShapeDataCache(60000); @@ -206,7 +203,7 @@ public abstract class AbstractShape extends WWObjectImpl implements OrderedRende /** * Returns the data cache entry for the current rendering. - * + * * @return the data cache entry for the current rendering. */ protected AbstractShapeData getCurrentData() { @@ -230,7 +227,7 @@ protected static class AbstractShapeData extends ShapeDataCache.ShapeDataCacheEn * Constructs a data cache entry and initializes its globe-dependent state key for the globe in the specified * draw context and capture the current vertical exaggeration. The entry becomes invalid when these values * change or when the entry's expiration timer expires. - * + * * @param dc * the current draw context. * @param minExpiryTime @@ -325,7 +322,7 @@ protected void reset() { /** * Returns this shape's normal (as opposed to highlight) attributes. - * + * * @return this shape's normal attributes. May be null. */ public ShapeAttributes getAttributes() { @@ -334,7 +331,7 @@ public ShapeAttributes getAttributes() { /** * Specifies this shape's normal (as opposed to highlight) attributes. - * + * * @param normalAttrs * the normal attributes. May be null, in which case default attributes are used. */ @@ -344,7 +341,7 @@ public void setAttributes(ShapeAttributes normalAttrs) { /** * Returns this shape's highlight attributes. - * + * * @return this shape's highlight attributes. May be null. */ public ShapeAttributes getHighlightAttributes() { @@ -353,7 +350,7 @@ public ShapeAttributes getHighlightAttributes() { /** * Specifies this shape's highlight attributes. - * + * * @param highlightAttrs * the highlight attributes. May be null, in which case default attributes are used. */ @@ -371,7 +368,7 @@ public void setHighlighted(boolean highlighted) { /** * Indicates whether this shape is drawn during rendering. - * + * * @return true if this shape is drawn, otherwise false. * @see #setVisible(boolean) */ @@ -381,7 +378,7 @@ public boolean isVisible() { /** * Specifies whether this shape is drawn during rendering. - * + * * @param visible * true to draw this shape, otherwise false. The default value is true. * @see #setAttributes(ShapeAttributes) @@ -393,7 +390,7 @@ public void setVisible(boolean visible) { /** * Returns this shape's altitude mode. If no altitude mode is set, this returns null to indicate that * the default altitude mode {@link AVKey#ABSOLUTE} is used. - * + * * @return this shape's altitude mode, or null indicating the default altitude mode. * @see #setAltitudeMode(String) */ @@ -409,7 +406,7 @@ public String getAltitudeMode() { * Note: If the altitude mode is unrecognized, {@link AVKey#ABSOLUTE} is used. *

* Note: Subclasses may recognize additional altitude modes or may not recognize the ones described above. - * + * * @param altitudeMode * the altitude mode. The default value is {@link AVKey#ABSOLUTE}. */ @@ -426,7 +423,7 @@ public double getDistanceFromEye() { /** * Indicates whether batch rendering is enabled for the concrete shape type of this shape. - * + * * @return true if batch rendering is enabled, otherwise false. * @see #setEnableBatchRendering(boolean). */ @@ -438,7 +435,7 @@ public boolean isEnableBatchRendering() { * Specifies whether adjacent shapes of this shape's concrete type in the ordered renderable list may be rendered * together if they are contained in the same layer. This increases performance. There is seldom a reason to disable * it. - * + * * @param enableBatchRendering * true to enable batch rendering, otherwise false. */ @@ -448,7 +445,7 @@ public void setEnableBatchRendering(boolean enableBatchRendering) { /** * Indicates whether batch picking is enabled. - * + * * @return true if batch rendering is enabled, otherwise false. * @see #setEnableBatchPicking(boolean). */ @@ -462,7 +459,7 @@ public boolean isEnableBatchPicking() { * polygons to be reported in a SelectEvent even if several of the polygons are at the pick position. *

* Batch rendering ({@link #setEnableBatchRendering(boolean)}) must be enabled in order for batch picking to occur. - * + * * @param enableBatchPicking * true to enable batch rendering, otherwise false. */ @@ -473,7 +470,7 @@ public void setEnableBatchPicking(boolean enableBatchPicking) { /** * Indicates the outline line width to use during picking. A larger width than normal typically makes the outline * easier to pick. - * + * * @return the outline line width used during picking. */ public int getOutlinePickWidth() { @@ -485,7 +482,7 @@ public int getOutlinePickWidth() { * easier to pick. *

* Note that the size of the pick aperture also affects the precision necessary to pick. - * + * * @param outlinePickWidth * the outline pick width. The default is 10. * @throws IllegalArgumentException @@ -504,7 +501,7 @@ public void setOutlinePickWidth(int outlinePickWidth) { /** * Indicates whether the filled sides of this shape should be offset towards the viewer to help eliminate artifacts * when two or more faces of this or other filled shapes are coincident. - * + * * @return true if depth offset is applied, otherwise false. */ public boolean isEnableDepthOffset() { @@ -514,7 +511,7 @@ public boolean isEnableDepthOffset() { /** * Specifies whether the filled sides of this shape should be offset towards the viewer to help eliminate artifacts * when two or more faces of this or other filled shapes are coincident. - * + * * @param enableDepthOffset * true if depth offset is applied, otherwise false. */ @@ -525,7 +522,7 @@ public void setEnableDepthOffset(boolean enableDepthOffset) { /** * Indicates the maximum length of time between geometry regenerations. See {@link #setGeometryRegenerationInterval(int)} for the regeneration-interval's * description. - * + * * @return the geometry regeneration interval, in milliseconds. * @see #setGeometryRegenerationInterval(int) */ @@ -540,7 +537,7 @@ public long getGeometryRegenerationInterval() { * from a server.) Decreasing this value causes the geometry to more quickly track terrain changes but at the cost * of performance. Increasing this value often does not have much effect because there are limiting factors other * than geometry regeneration. - * + * * @param geometryRegenerationInterval * the geometry regeneration interval, in milliseconds. The default is two * seconds. @@ -557,7 +554,7 @@ public void setGeometryRegenerationInterval(int geometryRegenerationInterval) { /** * Specifies the position to use as a reference position for computed geometry. This value should typically left to * the default value of the first position in the polygon's outer boundary. - * + * * @param referencePosition * the reference position. May be null, in which case the first position of the outer * boundary is the reference position. @@ -567,6 +564,11 @@ public void setReferencePosition(Position referencePosition) { this.reset(); } + @Override + public Layer getLayer() { + return this.layer; + } + public Object getDelegateOwner() { return delegateOwner; } @@ -577,7 +579,7 @@ public void setDelegateOwner(Object delegateOwner) { /** * Returns this shape's extent in model coordinates. - * + * * @return this shape's extent, or null if an extent has not been computed. */ public Extent getExtent() { @@ -587,7 +589,7 @@ public Extent getExtent() { /** * Returns the Cartesian coordinates of this shape's reference position as computed during the most recent * rendering. - * + * * @return the Cartesian coordinates corresponding to this shape's reference position, or null if the point has not * been computed. */ @@ -654,15 +656,24 @@ public void render(DrawContext dc) { this.shapeDataCache.addEntry(this.currentData); } - if (!this.isVisible()) return; + if (!this.isVisible()) { + Logging.debug("Not drawing object: !isVisible"); + return; + } if (this.isTerrainDependent()) this.checkViewDistanceExpiration(dc); if (this.getExtent() != null) { - if (!this.intersectsFrustum(dc)) return; + if (!this.intersectsFrustum(dc)) { + Logging.debug("Not drawing object: !intersectsFrustrum"); + return; + } // If the shape is less that a pixel in size, don't render it. - if (dc.isSmall(this.getExtent(), 1)) return; + if (dc.isSmall(this.getExtent(), 1)) { + Logging.debug("Not drawing object: isSmall"); + return; + } } if (dc.isOrderedRenderingMode()) this.drawOrderedRenderable(dc); @@ -674,7 +685,7 @@ public void render(DrawContext dc) { * renderable geometry. *

* A {@link gov.nasa.worldwind.render.AbstractShape.AbstractShapeData} must be current when this method is called. - * + * * @param dc * the current draw context. */ @@ -682,11 +693,16 @@ protected void makeOrderedRenderable(DrawContext dc) { // Re-use values already calculated this frame. if (dc.getFrameTimeStamp() != this.getCurrentData().getFrameNumber()) { this.determineActiveAttributes(); - if (this.getActiveAttributes() == null) return; + if (this.getActiveAttributes() == null) { + Logging.debug("Not drawing object: getActiveAttributes()==null"); + return; + } // Regenerate the positions and shape at a specified frequency. if (this.mustRegenerateGeometry(dc)) { - if (!this.doMakeOrderedRenderable(dc)) return; + if (!this.doMakeOrderedRenderable(dc)) { + return; + } this.fillVBO(dc); @@ -696,8 +712,12 @@ protected void makeOrderedRenderable(DrawContext dc) { this.getCurrentData().setFrameNumber(dc.getFrameTimeStamp()); } - if (!this.isOrderedRenderableValid(dc)) return; + if (!this.isOrderedRenderableValid(dc)) { + Logging.debug("Not drawing object: !isOrderedRenderableValid(dc)"); + return; + } + this.layer = dc.getCurrentLayer(); if (dc.isPickingMode()) this.pickLayer = dc.getCurrentLayer(); this.addOrderedRenderable(dc); @@ -705,7 +725,7 @@ protected void makeOrderedRenderable(DrawContext dc) { /** * Adds this shape to the draw context's ordered renderable list. - * + * * @param dc * the current draw context. */ @@ -716,7 +736,7 @@ protected void addOrderedRenderable(DrawContext dc) { /** * Determines which attributes -- normal, highlight or default -- to use each frame. Places the result in this * shape's current active attributes. - * + * * @see #getActiveAttributes() */ protected void determineActiveAttributes() { @@ -743,7 +763,7 @@ protected void determineActiveAttributes() { * attributes are either the normal or highlight attributes, depending on * this shape's highlight flag, and incorporates default attributes for those not specified in the applicable * attribute set. - * + * * @return this shape's currently active attributes. */ public ShapeAttributes getActiveAttributes() { @@ -755,7 +775,7 @@ public ShapeAttributes getActiveAttributes() { * property change or the expiration of the geometry regeneration interval. *

* A {@link gov.nasa.worldwind.render.AbstractShape.AbstractShapeData} must be current when this method is called. - * + * * @param dc * the current draw context. * @return true if this shape's geometry must be regenerated, otherwise false. @@ -766,7 +786,7 @@ protected boolean mustRegenerateGeometry(DrawContext dc) { /** * Indicates whether this shape's interior must be drawn. - * + * * @return true if an interior must be drawn, otherwise false. */ protected boolean mustDrawInterior() { @@ -775,7 +795,7 @@ protected boolean mustDrawInterior() { /** * Indicates whether this shape's outline must be drawn. - * + * * @return true if the outline should be drawn, otherwise false. */ protected boolean mustDrawOutline() { @@ -784,7 +804,7 @@ protected boolean mustDrawOutline() { /** * Indicates whether standard lighting must be applied. - * + * * @param dc * the current draw context * @return true if lighting must be applied, otherwise false. @@ -795,7 +815,7 @@ protected boolean mustApplyLighting(DrawContext dc) { /** * Indicates whether standard lighting must be applied. - * + * * @param dc * the current draw context * @param activeAttrs @@ -809,7 +829,7 @@ protected boolean mustApplyLighting(DrawContext dc, ShapeAttributes activeAttrs) /** * Indicates whether normal vectors must be computed by consulting the current active attributes. - * + * * @param dc * the current draw context * @return true if normal vectors must be computed, otherwise false. @@ -820,7 +840,7 @@ protected boolean mustCreateNormals(DrawContext dc) { /** * Indicates whether standard lighting must be applied. - * + * * @param dc * the current draw context * @param activeAttrs @@ -834,7 +854,7 @@ protected boolean mustCreateNormals(DrawContext dc, ShapeAttributes activeAttrs) /** * Indicates whether this shape's geometry depends on the terrain. - * + * * @return true if this shape's geometry depends on the terrain, otherwise false. */ protected boolean isTerrainDependent() { @@ -845,7 +865,7 @@ protected boolean isTerrainDependent() { * Indicates whether this shape's terrain-dependent geometry is continually computed as its distance from the eye * point changes. This is often necessary to ensure that the shape is updated as the terrain precision changes. But * it's often not necessary as well, and can be disabled. - * + * * @return true if the terrain dependent geometry is updated as the eye distance changes, otherwise false. The * default is true. */ @@ -857,7 +877,7 @@ public boolean isViewDistanceExpiration() { * Specifies whether this shape's terrain-dependent geometry is continually computed as its distance from the eye * point changes. This is often necessary to ensure that the shape is updated as the terrain precision changes. But * it's often not necessary as well, and can be disabled. - * + * * @param viewDistanceExpiration * true to enable view distance expiration, otherwise false. */ @@ -868,7 +888,7 @@ public void setViewDistanceExpiration(boolean viewDistanceExpiration) { /** * Determines whether this shape's geometry should be invalidated because the view distance changed, and if so, * invalidates the geometry. - * + * * @param dc * the current draw context. */ @@ -894,7 +914,7 @@ protected void checkViewDistanceExpiration(DrawContext dc) { * Determines whether this shape intersects the view frustum. *

* A {@link gov.nasa.worldwind.render.AbstractShape.AbstractShapeData} must be current when this method is called. - * + * * @param dc * the current draw context. * @return true if this shape intersects the frustum, otherwise false. @@ -903,8 +923,8 @@ protected boolean intersectsFrustum(DrawContext dc) { if (this.getExtent() == null) return true; // don't know the visibility, shape hasn't been computed yet // TODO: Restore these two lines after implementing World Wind Android picking frustums. - // if (dc.isPickingMode()) - // return dc.getPickFrustums().intersectsAny(this.getExtent()); + if (dc.isPickingMode()) + return dc.getPickFrustums().intersectsAny(this.getExtent()); return dc.getView().getFrustumInModelCoordinates().intersects(this.getExtent()); } @@ -913,7 +933,7 @@ protected boolean intersectsFrustum(DrawContext dc) { * Draws this shape as an ordered renderable. *

* A {@link gov.nasa.worldwind.render.AbstractShape.AbstractShapeData} must be current when this method is called. - * + * * @param dc * the current draw context. */ @@ -932,7 +952,7 @@ protected void drawOrderedRenderable(DrawContext dc) { * Establish the OpenGL state needed to draw this shape. *

* A {@link gov.nasa.worldwind.render.AbstractShape.AbstractShapeData} must be current when this method is called. - * + * * @param dc * the current draw context. */ @@ -946,18 +966,20 @@ protected void beginDrawing(DrawContext dc) { // Enable the gpu program's vertexPoint attribute, if one exists. The data for this attribute is specified by // each shape. - int attribLocation = program.getAttribLocation("vertexPoint"); - if (attribLocation >= 0) GLES20.glEnableVertexAttribArray(attribLocation); + program.enableVertexAttribute("vertexPoint"); + + program.loadUniform1f("uOpacity", dc.isPickingMode() ? 1f : this.layer.getOpacity()); // Set the OpenGL state that this shape depends on. GLES20.glDisable(GLES20.GL_CULL_FACE); + WorldWindowImpl.glCheckError("glDisable: GL_CULL_FACE"); } /** * Pop the state set in {@link #beginDrawing(DrawContext)}. *

* A {@link gov.nasa.worldwind.render.AbstractShape.AbstractShapeData} must be current when this method is called. - * + * * @param dc * the current draw context. */ @@ -968,21 +990,26 @@ protected void endDrawing(DrawContext dc) { // Disable the program's vertexPoint attribute, if one exists. This restores the program state modified in // beginRendering. This must be done while the program is still bound, because getAttribLocation depends on // the current OpenGL program state. - int location = program.getAttribLocation("vertexPoint"); - if (location >= 0) GLES20.glDisableVertexAttribArray(location); + program.disableVertexAttribute("vertexPoint"); // Restore the previous OpenGL program state. dc.setCurrentProgram(null); GLES20.glUseProgram(0); + WorldWindowImpl.glCheckError("glUseProgram"); // Restore the OpenGL array and element array buffer bindings to 0. GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + WorldWindowImpl.glCheckError("glBindBuffer"); GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0); + WorldWindowImpl.glCheckError("glBindBuffer"); // Restore the remaining OpenGL state values to their defaults. GLES20.glEnable(GLES20.GL_CULL_FACE); + WorldWindowImpl.glCheckError("glEnable: GL_CULL_FACE"); GLES20.glDepthMask(true); + WorldWindowImpl.glCheckError("glDepthMask"); GLES20.glLineWidth(1f); + WorldWindowImpl.glCheckError("glLineWidth"); } protected GpuProgram getDefaultGpuProgram(GpuResourceCache cache) { @@ -1010,7 +1037,7 @@ protected GpuProgram getDefaultGpuProgram(GpuResourceCache cache) { * current pick mode is true, only shapes within the same layer are drawn as a batch. *

* A {@link gov.nasa.worldwind.render.AbstractShape.AbstractShapeData} must be current when this method is called. - * + * * @param dc * the current draw context. */ @@ -1034,7 +1061,7 @@ protected void drawBatched(DrawContext dc) { if (!shape.isEnableBatchRendering() || !shape.isEnableBatchPicking()) break; if (shape.pickLayer != this.pickLayer) // batch pick only within a single layer - break; + break; dc.pollOrderedRenderables(); // take it off the queue shape.doDrawOrderedRenderable(dc, this.pickSupport); @@ -1051,7 +1078,7 @@ protected void drawBatched(DrawContext dc) { * rendered in batch are added to the same pick list. *

* A {@link gov.nasa.worldwind.render.AbstractShape.AbstractShapeData} must be current when this method is called. - * + * * @param dc * the current draw context. * @param pickCandidates @@ -1063,7 +1090,7 @@ protected void doDrawOrderedRenderable(DrawContext dc, PickSupport pickCandidate if (dc.isPickingMode()) { int color = dc.getUniquePickColor(); pickCandidates.addPickableObject(this.createPickedObject(color)); - dc.getCurrentProgram().loadUniformColor("color", this.currentColor.set(color, false)); // Ignore alpha. + dc.getCurrentProgram().loadUniformColor("uColor", this.currentColor.set(color, false)); // Ignore alpha. } this.applyModelviewProjectionMatrix(dc); @@ -1082,7 +1109,7 @@ protected void applyModelviewProjectionMatrix(DrawContext dc) { /** * Creates a {@link gov.nasa.worldwind.pick.PickedObject} for this shape and the specified unique pick color code. * The PickedObject returned by this method will be added to the pick list to represent the current shape. - * + * * @param colorCode * the unique color code for this shape. * @return a new picked object. @@ -1095,7 +1122,7 @@ protected PickedObject createPickedObject(int colorCode) { * Draws this shape's interior. *

* A {@link gov.nasa.worldwind.render.AbstractShape.AbstractShapeData} must be current when this method is called. - * + * * @param dc * the current draw context. */ @@ -1108,7 +1135,7 @@ protected void drawInterior(DrawContext dc) { /** * Establishes OpenGL state for drawing the interior, including setting the color/material. Enabling texture is left * to the subclass. - * + * * @param dc * the current draw context. * @param activeAttrs @@ -1125,13 +1152,14 @@ protected void prepareToDrawInterior(DrawContext dc, ShapeAttributes activeAttrs // Disable writing the shape's interior fragments to the OpenGL depth buffer when the interior is // semi-transparent. - if (color.a < 1) GLES20.glDepthMask(false); + if (color.a < 1 || activeAttrs.getInteriorOpacity()<1) GLES20.glDepthMask(false); + WorldWindowImpl.glCheckError("glDepthMask"); // Load the current interior color into the gpu program's color uniform variable. We first copy the outline // color into the current color so we can premultiply it. The SceneController configures the OpenGL blending // mode for premultiplied alpha colors. this.currentColor.set(color).premultiply(); - dc.getCurrentProgram().loadUniformColor("color", this.currentColor); + dc.getCurrentProgram().loadUniformColor("uColor", this.currentColor); } } @@ -1139,7 +1167,7 @@ protected void prepareToDrawInterior(DrawContext dc, ShapeAttributes activeAttrs * Draws this shape's outline. *

* A {@link gov.nasa.worldwind.render.AbstractShape.AbstractShapeData} must be current when this method is called. - * + * * @param dc * the current draw context. */ @@ -1154,7 +1182,7 @@ protected void drawOutline(DrawContext dc) { /** * Establishes OpenGL state for drawing the outline, including setting the color/material, line smoothing, line * width and stipple. Disables texture. - * + * * @param dc * the current draw context. * @param activeAttrs @@ -1173,16 +1201,17 @@ protected void prepareToDrawOutline(DrawContext dc, ShapeAttributes activeAttrs, // color into the current color so we can premultiply it. The SceneController configures the OpenGL blending // mode for premultiplied alpha colors. this.currentColor.set(color).premultiply(); - dc.getCurrentProgram().loadUniformColor("color", this.currentColor); + dc.getCurrentProgram().loadUniformColor("uColor", this.currentColor); } if (dc.isPickingMode() && activeAttrs.getOutlineWidth() < this.getOutlinePickWidth()) GLES20.glLineWidth(this.getOutlinePickWidth()); else GLES20.glLineWidth((float) activeAttrs.getOutlineWidth()); + WorldWindowImpl.glCheckError("glLineWidth"); } /** * Computes a model-coordinate point from a position, applying this shape's altitude mode. - * + * * @param terrain * the terrain to compute a point relative to the globe's surface. * @param position @@ -1203,7 +1232,7 @@ protected Vec4 computePoint(Terrain terrain, Position position) { /** * Computes this shape's approximate extent from its positions. - * + * * @param globe * the globe to use to compute the extent. * @param verticalExaggeration @@ -1240,7 +1269,7 @@ protected Extent computeExtentFromPositions(Globe globe, double verticalExaggera * Get or create OpenGL resource IDs for the current data cache entry. *

* A {@link gov.nasa.worldwind.render.AbstractShape.AbstractShapeData} must be current when this method is called. - * + * * @param dc * the current draw context. * @return an array containing the coordinate vertex buffer ID in the first position and the index vertex buffer ID @@ -1254,7 +1283,7 @@ protected int[] getVboIds(DrawContext dc) { * Removes from the GPU resource cache the entry for the current data cache entry's VBOs. *

* A {@link gov.nasa.worldwind.render.AbstractShape.AbstractShapeData} must be current when this method is called. - * + * * @param dc * the current draw context. */ @@ -1268,14 +1297,17 @@ protected int countTriangleVertices(List> prims, List pri for (int i = 0; i < prims.size(); i++) { switch (primTypes.get(i)) { case GLES20.GL_TRIANGLES: + WorldWindowImpl.glCheckError("GL_TRIANGLES"); numVertices += prims.get(i).size(); break; case GLES20.GL_TRIANGLE_FAN: + WorldWindowImpl.glCheckError("GL_TRIANGLE_FAN"); numVertices += (prims.get(i).size() - 2) * 3; // N tris from N + 2 vertices break; case GLES20.GL_TRIANGLE_STRIP: + WorldWindowImpl.glCheckError("GL_TRIANGLE_STRIP"); numVertices += (prims.get(i).size() - 2) * 3; // N tris from N + 2 vertices break; } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/render/AbstractSurfaceObject.java b/WorldWindAndroid/src/gov/nasa/worldwind/render/AbstractSurfaceObject.java new file mode 100644 index 0000000..c641432 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/render/AbstractSurfaceObject.java @@ -0,0 +1,892 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.render; + +import android.graphics.Point; +import gov.nasa.worldwind.R; +import gov.nasa.worldwind.WWObjectImpl; +import gov.nasa.worldwind.avlist.AVKey; +import gov.nasa.worldwind.cache.Cacheable; +import gov.nasa.worldwind.geom.*; +import gov.nasa.worldwind.globes.Globe; +import gov.nasa.worldwind.layers.Layer; +import gov.nasa.worldwind.pick.PickSupport; +import gov.nasa.worldwind.pick.PickedObject; +import gov.nasa.worldwind.util.*; + +import java.nio.FloatBuffer; +import java.nio.ShortBuffer; +import java.util.*; +import java.util.List; + +import android.opengl.GLES20; + +import static android.opengl.GLES20.*; + +/** + * Abstract implementation of SurfaceObject that participates in the {@link gov.nasa.worldwind.SceneController}'s bulk + * rendering of SurfaceObjects. The SceneControllers bulk renders all SurfaceObjects added to the {@link + * DrawContext}'s ordered surface renderable queue during the preRendering pass. While + * building the composite representation the SceneController invokes {@link #render(gov.nasa.worldwind.render.DrawContext)} in ordered rendering + * mode. To avoid overloading the purpose of the render method, AbstractSurfaceObject does not add itself to the + * DrawContext's ordered surface renderable queue during rendering. + *

+ * Subclasses that do not wish to participate in this composite representation can override this behavior as follows: + *

  1. Override {@link #makeOrderedPreRenderable(gov.nasa.worldwind.render.DrawContext)}; do not add this object to the draw context's + * ordered renderable queue. Perform any preRender processing necessary for the subclass to pick and render itself.
  2. + *
  3. Override {@link #pickOrderedRenderable(gov.nasa.worldwind.render.DrawContext, gov.nasa.worldwind.pick.PickSupport)}; draw the custom pick representation.
  4. + *
  5. Override {@link #makeOrderedRenderable(gov.nasa.worldwind.render.DrawContext)}; add this object to the draw context's ordered renderable + * queue. AbstractSurfaceObject does not add itself to this queue during rendering. render() is called from the + * SceneController while building the composite representation because AbstractSurfaceObject adds itself to the ordered + * surface renderable queue during preRendering.
  6. Override {@link #drawOrderedRenderable(gov.nasa.worldwind.render.DrawContext)}; draw the + * custom representation. Unlike AbstractSurfaceObject subclasses should assume the modelview and projection matrices + * are consistent with the current {@link gov.nasa.worldwind.View}.
+ * + * @author dcollins + * @version $Id: AbstractSurfaceObject.java 1171 2013-02-11 21:45:02Z dcollins $ + */ +public abstract class AbstractSurfaceObject extends WWObjectImpl implements SurfaceObject +{ + // Public interface properties. + protected boolean visible; + protected final long uniqueId; + protected long lastModifiedTime; + protected Object delegateOwner; + protected boolean enableBatchPicking; + protected boolean drawBoundingSectors; + protected Map extentCache = new HashMap(); + // Picking properties. + protected Layer layer; + protected PickSupport pickSupport = new PickSupport(); + /** Support class used to build surface tiles used to draw the pick representation. */ + protected SurfaceObjectTileBuilder pickTileBuilder; + /** The pick representation. Populated each frame by the {@link #pickTileBuilder}. */ + protected List pickTiles; + /* The next unique ID. This property is shared by all instances of AbstractSurfaceObject. */ + protected static long nextUniqueId = 1; + + /** + * Creates a new AbstractSurfaceObject, assigning it a unique ID and initializing its last modified time to the + * current system time. + */ + public AbstractSurfaceObject() + { + this.visible = true; + this.uniqueId = nextUniqueId(); + this.lastModifiedTime = System.currentTimeMillis(); + this.enableBatchPicking = true; + } + + /** + * Returns the next unique integer associated with an AbstractSurfaceObject. This method is synchronized to ensure + * that two threads calling simultaneously receive different values. Since this method is called from + * AbstractSurfaceObject's constructor, this is critical to ensure that AbstractSurfaceObject can be safely + * constructed on separate threads. + * + * @return the next unique integer. + */ + protected static synchronized long nextUniqueId() + { + return nextUniqueId++; + } + + @Override + public boolean isVisible() + { + return this.visible; + } + + @Override + public void setVisible(boolean visible) + { + this.visible = visible; + this.updateModifiedTime(); + } + + @Override + public Object getStateKey(DrawContext dc) + { + return new SurfaceObjectStateKey(this.getUniqueId(), this.lastModifiedTime); + } + + @Override + public double getDistanceFromEye() + { + return 0; + } + + @Override + public Object getDelegateOwner() + { + return this.delegateOwner; + } + + @Override + public void setDelegateOwner(Object owner) + { + this.delegateOwner = owner; + } + + /** + * Indicates whether the SurfaceObject draws its bounding sector. + * + * @return true if the shape draws its outline; false otherwise. + * + * @see #setDrawBoundingSectors(boolean) + */ + public boolean isDrawBoundingSectors() + { + return this.drawBoundingSectors; + } + + /** + * Specifies if the SurfaceObject should draw its bounding sector. If true, the SurfaceObject draws an + * outline of its bounding sector in green on top of its shape. The default value is false. + * + * @param draw true to draw the shape's outline; false otherwise. + */ + public void setDrawBoundingSectors(boolean draw) + { + this.drawBoundingSectors = draw; + // Update the modified time so the object's composite representation is updated. We draw the bounding sector + // along with the object's composite representation. + this.updateModifiedTime(); + } + + /** + * Indicates whether batch picking is enabled. + * + * @return true to enable batch picking; false otherwise. + * + * @see #setEnableBatchPicking(boolean) + */ + public boolean isEnableBatchPicking() + { + return this.enableBatchPicking; + } + + /** + * Specifies whether adjacent SurfaceObjects in the DrawContext's ordered surface renderable list may be rendered + * together during picking if they are contained in the same layer. This increases performance and there is seldom a + * reason to disable it. + * + * @param enable true to enable batch picking; false otherwise. + */ + public void setEnableBatchPicking(boolean enable) + { + this.enableBatchPicking = enable; + } + + public Extent getExtent(DrawContext dc) + { + if (dc == null) + { + String message = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + CacheEntry entry = this.extentCache.get(dc.getGlobe()); + if (entry != null && entry.isValid(dc)) + { + return (Extent) entry.object; + } + else + { + entry = new CacheEntry(this.computeExtent(dc), dc); + this.extentCache.put(dc.getGlobe(), entry); + return (Extent) entry.object; + } + } + + @Override + public void preRender(DrawContext dc) + { + if (dc == null) + { + String message = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (!this.isVisible()) + return; + + // Ordered pre-rendering is a no-op for this object. The SceneController prepares a composite representation of + // this object for rendering and calls this object's render method when doing so. + if (!dc.isOrderedRenderingMode()) + this.makeOrderedPreRenderable(dc); + } + + @Override + public void pick(DrawContext dc, Point pickPoint) + { + if (dc == null) + { + String message = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // This method is called only during ordered picking. Therefore we setup for picking and draw this object to the + // framebuffer in a unique pick color. We invoke a separate path for picking because this object creates and + // draws a separate representation of itself during picking. Using a separate call stack enables us to use + // common rendering code to draw both the pick and render representations, by setting the draw context's + // isPickingMode flag to control which representation is drawn. + + if (!this.isVisible()) + return; + + this.pickSupport.clearPickList(); + try + { + this.pickSupport.beginPicking(dc); + this.pickOrderedRenderable(dc, this.pickSupport); + + if (this.isEnableBatchPicking()) + this.pickBatched(dc, this.pickSupport); + } + finally + { + this.pickSupport.endPicking(dc); + this.pickSupport.resolvePick(dc, pickPoint, this.layer); + } + } + + @Override + public void render(DrawContext dc) + { + if (dc == null) + { + String message = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (!this.isVisible()) + return; + + if (dc.isOrderedRenderingMode()) + this.drawOrderedRenderable(dc); + else + this.makeOrderedRenderable(dc); + } + + /** + * Returns an integer number uniquely identifying the surface object. This number is unique relative to other + * instances of SurfaceObject, but is not necessarily globally unique. + * + * @return the surface object's unique identifier. + */ + protected long getUniqueId() + { + return this.uniqueId; + } + + /** + * Sets the SurfaceObject's modified time to the current system time. This causes cached representations of this + * SurfaceObject to be refreshed. + */ + protected void updateModifiedTime() + { + this.lastModifiedTime = System.currentTimeMillis(); + } + + /** Clears this SurfaceObject's internal extent cache. */ + protected void clearCaches() + { + this.extentCache.clear(); + } + + //**************************************************************// + //******************** Extent ********************************// + //**************************************************************// + + /** + * Computes the surface object's extent. Uses the sector list returned by {@link #getSectors(gov.nasa.worldwind.render.DrawContext)} to + * compute the extent bounding this object's sectors on the draw context's surface. This returns null if the surface + * object has no sectors. + * + * @param dc the draw context the extent relates to. + * + * @return the surface object's extent. Returns null if the surface object has no sectors. + */ + protected Extent computeExtent(DrawContext dc) + { + List sectors = this.getSectors(dc); + if (sectors == null) + return null; + + return this.computeExtent(dc.getGlobe(), dc.getVerticalExaggeration(), sectors); + } + + /** + * Computes an extent bounding the the specified sectors on the specified Globe's surface. If the list contains a + * single sector this returns a box created by calling {@link gov.nasa.worldwind.geom.Sector#computeBoundingBox(gov.nasa.worldwind.globes.Globe, + * double, gov.nasa.worldwind.geom.Sector)}. If the list contains more than one sector this returns a {@link + * gov.nasa.worldwind.geom.Box} containing the corners of the boxes bounding each sector. This returns null if the + * sector list is empty. + * + * @param globe the globe the extent relates to. + * @param verticalExaggeration the globe's vertical surface exaggeration. + * @param sectors the sectors to bound. + * + * @return an extent for the specified sectors on the specified Globe. + */ + protected Extent computeExtent(Globe globe, double verticalExaggeration, List sectors) + { + // This should never happen, but we check anyway. + if (sectors.size() == 0) + { + return null; + } + // This surface shape does not cross the international dateline, and therefore has a single bounding sector. + // Return the box which contains that sector. + else if (sectors.size() == 1) + { + return Sector.computeBoundingBox(globe, verticalExaggeration, sectors.get(0)); + } + // This surface crosses the international dateline, and its bounding sectors are split along the dateline. + // Return a box which contains the corners of the boxes bounding each sector. + else + { + ArrayList boxCorners = new ArrayList(); + + for (Sector s : sectors) + { + Box box = Sector.computeBoundingBox(globe, verticalExaggeration, s); + boxCorners.addAll(Arrays.asList(box.getCorners())); + } + + return Box.computeBoundingBox(boxCorners); + } + } + + /** + * Test if this SurfaceObject intersects the specified draw context's frustum. During picking mode, this tests + * intersection against all of the draw context's pick frustums. During rendering mode, this tests intersection + * against the draw context's viewing frustum. + * + * @param dc the draw context the SurfaceObject is related to. + * + * @return true if this SurfaceObject intersects the draw context's frustum; false otherwise. + */ + protected boolean intersectsFrustum(DrawContext dc) + { + // A null extent indicates an object which has no location. + Extent extent = this.getExtent(dc); + if (extent == null) + return false; + + // Test this object's extent against the pick frustum list + if (dc.isPickingMode()) + return dc.getPickFrustums().intersectsAny(extent); + + // Test this object's extent against the viewing frustum. + return dc.getView().getFrustumInModelCoordinates().intersects(extent); + } + + /** + * Test if this SurfaceObject intersects the specified draw context's pick frustums. + * + * @param dc the draw context the SurfaceObject is related to. + * + * @return true if this SurfaceObject intersects any of the draw context's pick frustums; false otherwise. + */ + protected boolean intersectsPickFrustum(DrawContext dc) + { + // Test this object's extent against the pick frustum list. A null extent indicates the object has no location. + Extent extent = this.getExtent(dc); + return extent != null && dc.getPickFrustums().intersectsAny(extent); + } + + /** + * Test if this SurfaceObject intersects the specified draw context's visible sector. This returns false if the draw + * context's visible sector is null. + * + * @param dc draw context the SurfaceObject is related to. + * + * @return true if this SurfaceObject intersects the draw context's visible sector; false otherwise. + */ + protected boolean intersectsVisibleSector(DrawContext dc) + { + if (dc.getVisibleSector() == null) + return false; + + List sectors = this.getSectors(dc); + if (sectors == null) + return false; + + for (Sector s : sectors) + { + if (s.intersects(dc.getVisibleSector())) + return true; + } + + return false; + } + + //**************************************************************// + //******************** Rendering *****************************// + //**************************************************************// + + /** + * Prepares the SurfaceObject as an {@link OrderedRenderable} and adds it to the + * DrawContext's ordered surface renderable list. Additionally, this prepares the SurfaceObject's pickable + * representation if the SurfaceObject's containing layer is enabled for picking and the SurfaceObject intersects + * one of the DrawContext's picking frustums. + *

+ * During ordered preRendering, the {@link gov.nasa.worldwind.SceneController} builds a composite representation of + * this SurfaceObject and any other SurfaceObject on the DrawContext's ordered surface renderable list. The + * SceneController causes each SurfaceObject's to draw itself into the composite representation by calling its + * {@link #render(gov.nasa.worldwind.render.DrawContext)} method in ordered rendering mode. + * + * @param dc the DrawContext to add to. + */ + protected void makeOrderedPreRenderable(DrawContext dc) + { + this.layer = dc.getCurrentLayer(); + // Test for visibility against the draw context's visible sector prior to preparing this object for + // preRendering. + if (!this.intersectsVisibleSector(dc)) + return; + + // Create a representation of this object that can be used during picking. No need for a pickable representation + // if this object's parent layer isn't pickable or if this object doesn't intersect the pick frustum. We do not + // test visibility against the view frustum, because it's possible for the pick frustum to slightly exceed the + // view frustum when the cursor is on the viewport edge. + if ((dc.getCurrentLayer() == null || dc.getCurrentLayer().isPickEnabled()) && this.intersectsPickFrustum(dc)) + this.buildPickRepresentation(dc); + + // If this object is visible, add it to the draw context's ordered surface renderable queue. This queue is + // processed by the SceneController during the preRender pass as follows: the SceneController builds a composite + // representation of this object and any other SurfaceObject on the queue, and calls this object's preRender + // method (we ignore this call with a conditional in preRender). While building a composite representation the + // SceneController calls this object's render method in ordered rendering mode. + if (this.intersectsFrustum(dc)) + dc.addOrderedSurfaceRenderable(this); + } + + /** + * Prepares the SurfaceObject as an {@link OrderedRenderable} and adds it to the + * DrawContext's ordered surface renderable list. We ignore this call during rendering mode to suppress calls to + * {@link #render(gov.nasa.worldwind.render.DrawContext)} during ordered rendering mode. The SceneController already invokes render during + * ordered picking mode to build a composite representation of the SurfaceObjects. + *

+ * During ordered picking, the {@link gov.nasa.worldwind.SceneController} invokes the SurfaceObject's {@link + * #pick(gov.nasa.worldwind.render.DrawContext, android.graphics.Point)} method. + * + * @param dc the DrawContext to add to. + */ + protected void makeOrderedRenderable(DrawContext dc) + { + this.layer = dc.getCurrentLayer(); + // Add this object to the draw context's ordered surface renderable queue only during picking mode. This queue + // is processed by the SceneController during each rendering pass as follows: + // + // 1) Ordered picking - the SceneController calls this object's pick method. + // + // 2) Ordered rendering - the SceneController draws the composite representation of this object prepared during + // ordered preRendering and calls this object's render method. Since we use the render method to draw a + // composite representation during preRendering, we suppress this call by not adding to the ordered surface + // renderable queue during rendering. + + if (!dc.isPickingMode()) + return; + + // Test for visibility prior to adding this object to the draw context's ordered renderable queue. Note that + // there's no need to test again during ordered rendering mode. + if (!this.intersectsVisibleSector(dc) || !this.intersectsFrustum(dc)) + return; + + dc.addOrderedSurfaceRenderable(this); + } + + /** + * Causes the SurfaceObject to draw itself in a unique pick color, and add itself as a pickable object to the + * specified pickSupport. + * + * @param dc the current DrawContext. + * @param pickSupport the PickSupport to add the SurfaceObject to. + */ + protected void pickOrderedRenderable(DrawContext dc, PickSupport pickSupport) + { + // Register a unique pick color with the PickSupport. We define the pickable object to be the caller specified + // delegate owner, or this object if the delegate owner is null. We define the picked position to be the + // terrain's picked position to maintain backwards compatibility with previous implementations of SurfaceObject. + Color pickColor = new Color(dc.getUniquePickColor()); + pickSupport.addPickableObject(this.createPickedObject(dc, pickColor)); + + // Draw an individual representation of this object in a unique pick color. This representation is created + // during the preRender pass in makeOrderedPreRenderable(). + dc.getCurrentProgram().loadUniformColor("uColor", pickColor); + this.drawPickRepresentation(dc); + } + + /** + * Create a {@link gov.nasa.worldwind.pick.PickedObject} for this surface object. The PickedObject created by this + * method will be added to the pick list to represent the current surface object. + * + * @param dc Active draw context. + * @param pickColor Unique color for this PickedObject. + * + * @return A new picked object. + */ + protected PickedObject createPickedObject(DrawContext dc, Color pickColor) + { + Object pickedObject = this.getDelegateOwner() != null ? this.getDelegateOwner() : this; + Position pickedPos = dc.getObjectsAtPickPoint().getTerrainObject() != null + ? dc.getObjectsAtPickPoint().getTerrainObject().getPosition() : null; + + return new PickedObject(pickColor.toColorInt(), pickedObject, pickedPos, false); + } + + /** + * Causes adjacent SurfaceObjects in the DrawContext's ordered surface renderable list to draw themselves in in a + * unique pick color, and adds themselves as pickable objects to the specified pickSupport. Adjacent SurfaceObjects + * are removed from the DrawContext's list and processed until one is encountered that has a different containing + * layer or is not enabled for batch picking. + * + * @param dc the current DrawContext. + * @param pickSupport the PickSupport to add the SurfaceObject to. + */ + protected void pickBatched(DrawContext dc, PickSupport pickSupport) + { + // Draw as many as we can in a batch to save pick resolution. + Object nextItem = dc.getOrderedSurfaceRenderables().peek(); + + while (nextItem != null && nextItem instanceof AbstractSurfaceObject) + { + AbstractSurfaceObject so = (AbstractSurfaceObject) nextItem; + + // Batch pick only within a single layer, and for objects which are enabled for batch picking. + if (so.layer != this.layer || !so.isEnableBatchPicking()) + break; + + dc.getOrderedSurfaceRenderables().poll(); // take it off the queue + so.pickOrderedRenderable(dc, pickSupport); + + nextItem = dc.getOrderedSurfaceRenderables().peek(); + } + } + + /** + * Causes the SurfaceObject to render itself. SurfaceObjects are drawn in geographic coordinates into offscreen + * surface tiles. This attempts to get a {@link gov.nasa.worldwind.util.SurfaceTileDrawContext} from the + * DrawContext's AVList by querying the key {@link gov.nasa.worldwind.avlist.AVKey#SURFACE_TILE_DRAW_CONTEXT}. If + * the DrawContext has a SurfaceTileDrawContext attached under that key, this calls {@link + * #drawGeographic(gov.nasa.worldwind.render.DrawContext, gov.nasa.worldwind.util.SurfaceTileDrawContext)} with the SurfaceTileDrawContext. + * Otherwise this logs a warning and returns. + * + * @param dc the current DrawContext. + */ + protected void drawOrderedRenderable(DrawContext dc) + { + // This method is invoked by the SceneController during ordered rendering mode while building a composite + // representation of the SurfaceObjects during ordered preRendering. Since we use this method to draw a + // composite representation during preRendering, we prevent this method from being invoked during ordered + // rendering. Note that this method is not invoked during ordered picking; pickOrderedRenderable is called + // instead. + + SurfaceTileDrawContext sdc = (SurfaceTileDrawContext) dc.getValue(AVKey.SURFACE_TILE_DRAW_CONTEXT); + if (sdc == null) + { + Logging.warning(Logging.getMessage("nullValue.SurfaceTileDrawContextIsNull")); + return; + } + + this.drawGeographic(dc, sdc); + + // Draw the diagnostic bounding sectors during ordered rendering mode. + if (this.isDrawBoundingSectors() && !dc.isPickingMode()) + this.drawBoundingSectors(dc, sdc); + } + + /** + * Causes the SurfaceObject to render itself to the specified region in geographic coordinates. The specified + * viewport denotes the geographic region and its corresponding screen viewport. + * + * @param dc the current draw context. + * @param sdc the context containing a geographic region and screen viewport corresponding to a surface tile. + */ + protected abstract void drawGeographic(DrawContext dc, SurfaceTileDrawContext sdc); + + //**************************************************************// + //******************** Picking *******************************// + //**************************************************************// + + /** + * Builds this AbstractSurfaceObject's pickable representation. This method is called during the preRender phase, + * and is therefore free to modify the framebuffer contents to create the pickable representation. + * + * @param dc the draw context to build a representation for. + */ + protected void buildPickRepresentation(DrawContext dc) + { + // Lazily create the collection used to hold the pick representation's SurfaceTiles. This ensures the collection + // does not waste memory for surface objects that are never picked. + if (this.pickTiles == null) + this.pickTiles = new ArrayList(); + + // Lazily create the support object used to build the pick representation. We keep a reference to the + // SurfaceObjectTileBuilder used to build the tiles because it acts as a cache key to the tiles and determines + // when the tiles must be updated. + if (this.pickTileBuilder == null) + this.pickTileBuilder = this.createPickTileBuilder(); + + // Clear any SurfaceTiles from the pick representation build during the previous frame. + this.pickTiles.clear(); + + // Build the pickable representation of this surface object as a list of surface tiles. Set the DrawContext into + // ordered picking mode while the surface object's pickable representation is built. During ordered picking mode + // the surface objects draws a pickable representation of itself in the surface tile's, and culls tiles against + // the pick frustum. + boolean prevPickingMode = dc.isPickingMode(); + boolean prevOrderedRenderingMode = dc.isOrderedRenderingMode(); + try + { + if (!prevPickingMode) + dc.setPickingMode(true); + dc.setOrderedRenderingMode(true); + + // Build the pick representation as a list of surface tiles. + List tiles = this.pickTileBuilder.buildTiles(dc, Arrays.asList(this)); + if (tiles != null) + this.pickTiles.addAll(tiles); + } + finally + { + // Restore the DrawContext's previous picking and ordered rendering modes. + if (!prevPickingMode) + dc.setPickingMode(false); + dc.setOrderedRenderingMode(prevOrderedRenderingMode); + } + } + + /** + * Causes this SurfaceObject to draw a representation of itself suitable for use during picking. + * + * @param dc the current DrawContext. + */ + protected void drawPickRepresentation(DrawContext dc) + { + // The pick representation is stored as a list of surface tiles. If the list is empty, then this surface object + // was not picked. This method might be called when the list is null or empty because of an upstream + // exception that prevented creation of the list. + if (this.pickTiles == null || this.pickTiles.isEmpty()) + return; + + // Draw the pickable representation of this surface object created during preRendering. + OGLStackHandler ogsh = new OGLStackHandler(); + ogsh.pushAttrib(OGLStackHandler.GL_POLYGON_BIT); // For cull face enable, cull face, polygon mode. + try + { + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + + dc.getSurfaceTileRenderer().renderTiles(dc, this.pickTiles); + } + finally + { + ogsh.popAttrib(OGLStackHandler.GL_POLYGON_BIT); + // Clear the list of pick tiles to avoid retaining references to them in case we're never picked again. + this.pickTiles.clear(); + } + } + + /** + * Returns a {@link gov.nasa.worldwind.render.SurfaceObjectTileBuilder} appropriate for building and drawing the surface object's pickable + * representation. The returned SurfaceObjectTileBuilder's is configured to create textures with the GL_ALPHA8 + * format, and to use GL_NEAREST filtering. This reduces a surface object's pick texture resources by a factor of 4, + * and ensures that linear texture filtering and mip-mapping is disabled while drawing the pick tiles. + * + * @return a SurfaceObjectTileBuilder used for building and drawing the surface object's pickable representation. + */ + protected SurfaceObjectTileBuilder createPickTileBuilder() + { + return new SurfaceObjectTileBuilder(new Point(512, 512), GL_ALPHA, false, false); + } + + //**************************************************************// + //******************** Diagnostic Support ********************// + //**************************************************************// + + protected FloatBuffer sectorVertexBuffer; + protected ShortBuffer sectorIndexBuffer; + + /** + * Causes this SurfaceObject to render its bounding sectors to the specified region in geographic coordinates. The + * specified viewport denotes the geographic region and its corresponding screen viewport. + *

+ * The bounding sectors are rendered as a 1 pixel wide green outline. + * + * @param dc the current DrawContext. + * @param sdc the context containing a geographic region and screen viewport corresponding to a surface tile. + * + * @see #getSectors(gov.nasa.worldwind.render.DrawContext) + */ + protected void drawBoundingSectors(DrawContext dc, SurfaceTileDrawContext sdc) + { + List sectors = this.getSectors(dc); + if (sectors == null) + return; + + //TODO don't allocate vertex buffer each frame, but in advance. Maybe based on max # of sectors? + sectorVertexBuffer = BufferUtil.newFloatBuffer(4*8*sectors.size()); + sectorVertexBuffer.rewind(); + sectorIndexBuffer = BufferUtil.newShortBuffer(2*8*sectors.size()); + sectorIndexBuffer.rewind(); + short indexOffset = 0; + for (Sector s : sectors) + { + LatLon[] corners = s.getCorners(); + sectorVertexBuffer.put((float) corners[0].getLongitude().degrees); + sectorVertexBuffer.put((float) corners[0].getLatitude().degrees); + sectorVertexBuffer.put((float) corners[1].getLongitude().degrees); + sectorVertexBuffer.put((float) corners[1].getLatitude().degrees); + sectorVertexBuffer.put((float) corners[2].getLongitude().degrees); + sectorVertexBuffer.put((float) corners[2].getLatitude().degrees); + sectorVertexBuffer.put((float) corners[3].getLongitude().degrees); + sectorVertexBuffer.put((float) corners[3].getLatitude().degrees); + sectorIndexBuffer.put((short)(indexOffset+0)); + sectorIndexBuffer.put((short)(indexOffset+1)); + sectorIndexBuffer.put((short)(indexOffset+1)); + sectorIndexBuffer.put((short)(indexOffset+2)); + sectorIndexBuffer.put((short)(indexOffset+2)); + sectorIndexBuffer.put((short)(indexOffset+3)); + sectorIndexBuffer.put((short)(indexOffset+3)); + sectorIndexBuffer.put((short)(indexOffset+0)); + indexOffset+=4; + } + + GpuProgram program = WWIO.getGpuProgram(dc.getGpuResourceCache(), R.raw.uniform_color_frag, R.raw.simple_vert, R.raw.uniform_color_frag); + + OGLStackHandler ogsh = new OGLStackHandler(); + ogsh.pushAttrib(GL_COLOR_BUFFER_BIT); // For alpha test enable, blend enable, alpha func, blend func. + try + { + glEnable(GL_BLEND); + OGLUtil.applyBlending(false); + + glLineWidth(1f); + + program.loadUniformColor("uColor", new Color(1f, 1f, 1f, 0.5f)); + + // Set the model-view matrix to transform from geographic coordinates to viewport coordinates. + Matrix mvp = Matrix.fromIdentity(); + + mvp.multiplyAndSet(sdc.getProjectionMatrix(), sdc.getModelviewMatrix()); + program.loadUniformMatrix("mvp", mvp); + + program.enableVertexAttribute("vertexPoint"); + program.vertexAttribPointer("vertexPoint", 2, GL_FLOAT, false, 2*4, sectorVertexBuffer.rewind()); + glDrawElements(GL_LINES, 8*sectors.size(), GL_UNSIGNED_SHORT, sectorIndexBuffer.rewind()); +// glDrawArrays(GL_LINES, 0, 8*sectors.size()); + } + finally + { + ogsh.popAttrib(); + } + } + + @Override + public Layer getLayer() { + return this.layer; + } + + //**************************************************************// + //******************** State Key *****************************// + //**************************************************************// + + /** + * Represents a surface object's current state. StateKey uniquely identifies a surface object's current state as + * follows:

  • The StateKey class distinguishes the key from other object types.
  • The object's unique + * ID distinguishes one surface object instances from another.
  • The object's modified time distinguishes an + * object's internal state from any of its previous states.
  • Using the unique ID to distinguish between objects + * ensures that the StateKey does not store dangling references to the surface object itself. Should the StateKey + * live longer than the surface object that created it, the StateKey does not prevent the object from being garbage + * collected. + */ + protected static class SurfaceObjectStateKey implements Cacheable + { + /** The SurfaceObject's unique ID. This is unique to all instances of SurfaceObject. */ + protected final long uniqueId; + /** The SurfaceObject's modified time. */ + protected final long modifiedTime; + + /** + * Constructs a new SurfaceObjectStateKey with the specified unique ID and modified time. + * + * @param uniqueId the SurfaceObject's unique ID. + * @param modifiedTime the SurfaceObject's modified time. + */ + public SurfaceObjectStateKey(long uniqueId, long modifiedTime) + { + this.uniqueId = uniqueId; + this.modifiedTime = modifiedTime; + } + + @Override + @SuppressWarnings({"SimplifiableIfStatement"}) + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || this.getClass() != o.getClass()) + return false; + + SurfaceObjectStateKey that = (SurfaceObjectStateKey) o; + return this.uniqueId == that.uniqueId && this.modifiedTime == that.modifiedTime; + } + + @Override + public int hashCode() + { + return 31 * (int) (this.uniqueId ^ (this.uniqueId >>> 32)) + + (int) (this.modifiedTime ^ (this.modifiedTime >>> 32)); + } + + /** + * Returns the state key's size in bytes. + * + * @return the state key's size in bytes. + */ + public long getSizeInBytes() + { + return 16; // Return the size of two long integers. + } + } + + //**************************************************************// + //******************** Cache Entry ***************************// + //**************************************************************// + + /** Represents a globe dependent cache entry. */ + protected static class CacheEntry + { + public Object object; + protected Object globeStateKey; + + public CacheEntry(Object object, DrawContext dc) + { + this.object = object; + this.globeStateKey = dc.getGlobe().getStateKey(dc); + } + + /** + * Determines if this cache entry is valid for the specified drawing context. The entry depends on the state of + * the globe used to compute it. + * + * @param dc the current drawing context. + * + * @return true if this cache entry is valid; false otherwise; + */ + protected boolean isValid(DrawContext dc) + { + return this.globeStateKey.equals(dc.getGlobe().getStateKey(dc)); + } + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/render/AbstractSurfaceShape.java b/WorldWindAndroid/src/gov/nasa/worldwind/render/AbstractSurfaceShape.java new file mode 100644 index 0000000..d7a3aa8 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/render/AbstractSurfaceShape.java @@ -0,0 +1,1816 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.render; + +import android.opengl.GLES20; +import gov.nasa.worldwind.Movable; +import gov.nasa.worldwind.R; +import gov.nasa.worldwind.WorldWindowImpl; +import gov.nasa.worldwind.avlist.AVKey; +import gov.nasa.worldwind.geom.*; +import gov.nasa.worldwind.globes.Globe; +import gov.nasa.worldwind.util.*; +import gov.nasa.worldwind.util.measure.AreaMeasurer; + +import java.nio.FloatBuffer; +import java.util.*; + +import static android.opengl.GLES20.*; +import static gov.nasa.worldwind.util.OGLStackHandler.GL_POLYGON_BIT; + +/** + * Common superclass for surface conforming shapes such as {@link gov.nasa.worldwind.render.SurfacePolygon}, {@link + * gov.nasa.worldwind.render.SurfacePolyline}, {@link gov.nasa.worldwind.render.SurfaceEllipse}, {@link + * gov.nasa.worldwind.render.SurfaceQuad}, and {@link gov.nasa.worldwind.render.SurfaceSector}. + *

    + * SurfaceShapes have separate attributes for normal display and highlighted display. If no attributes are specified, + * default attributes are used. See {@link #DEFAULT_INTERIOR_MATERIAL}, {@link #DEFAULT_OUTLINE_MATERIAL}, and {@link + * #DEFAULT_HIGHLIGHT_MATERIAL}. + *

    + * AbstractSurfaceShape extends from {@link AbstractSurfaceObject}, and therefore inherits + * AbstractSurfaceObject's batch rendering capabilities. + * + * @author dcollins + * @version $Id: AbstractSurfaceShape.java 1869 2014-03-14 23:03:14Z dcollins $ + */ +public abstract class AbstractSurfaceShape extends AbstractSurfaceObject implements SurfaceShape, Movable +{ + /** The default interior color. */ + protected static final Material DEFAULT_INTERIOR_MATERIAL = Material.PINK; + /** The default outline color. */ + protected static final Material DEFAULT_OUTLINE_MATERIAL = Material.RED; + /** The default highlight color. */ + protected static final Material DEFAULT_HIGHLIGHT_MATERIAL = Material.WHITE; + /** The default path type. */ + protected static final String DEFAULT_PATH_TYPE = AVKey.GREAT_CIRCLE; + /** The default number of texels per shape edge interval. */ + protected static final int DEFAULT_TEXELS_PER_EDGE_INTERVAL = 50; + /** The default minimum number of shape edge intervals. */ + protected static final int DEFAULT_MIN_EDGE_INTERVALS = 0; + /** The default maximum number of shape edge intervals. */ + protected static final int DEFAULT_MAX_EDGE_INTERVALS = 100; + /** The attributes used if attributes are not specified. */ + protected static final ShapeAttributes defaultAttrs; + + static + { + defaultAttrs = new BasicShapeAttributes(); + defaultAttrs.setInteriorMaterial(DEFAULT_INTERIOR_MATERIAL); + defaultAttrs.setOutlineMaterial(DEFAULT_OUTLINE_MATERIAL); + } + + // Public interface properties. + protected boolean highlighted; + protected ShapeAttributes normalAttrs; + protected ShapeAttributes highlightAttrs; + protected ShapeAttributes activeAttrs = this.createActiveAttributes(); // re-determined each frame + protected String pathType = DEFAULT_PATH_TYPE; + protected double texelsPerEdgeInterval = DEFAULT_TEXELS_PER_EDGE_INTERVAL; + protected int minEdgeIntervals = DEFAULT_MIN_EDGE_INTERVALS; + protected int maxEdgeIntervals = DEFAULT_MAX_EDGE_INTERVALS; + // Rendering properties. + protected List> activeGeometry = new ArrayList>(); // re-determined each frame + protected List> activeOutlineGeometry = new ArrayList>(); // re-determined each frame + protected GpuTexture texture; // An optional texture. + protected Map sectorCache = new HashMap(); + protected Map geometryCache = new HashMap(); + protected OGLStackHandler stackHandler = new OGLStackHandler(); + protected static FloatBuffer vertexBuffer; + // Measurement properties. + protected AreaMeasurer areaMeasurer; + protected long areaMeasurerLastModifiedTime; + + /** Constructs a new surface shape with the default attributes. */ + public AbstractSurfaceShape() + { + } + + /** + * Constructs a new surface shape with the specified normal (as opposed to highlight) attributes. Modifying the + * attribute reference after calling this constructor causes this shape's appearance to change accordingly. + * + * @param normalAttrs the normal attributes. May be null, in which case default attributes are used. + */ + public AbstractSurfaceShape(ShapeAttributes normalAttrs) + { + this.setAttributes(normalAttrs); + } + + /** {@inheritDoc} */ + public boolean isHighlighted() + { + return this.highlighted; + } + + /** {@inheritDoc} */ + public void setHighlighted(boolean highlighted) + { + this.highlighted = highlighted; + this.updateModifiedTime(); + } + + /** {@inheritDoc} */ + public ShapeAttributes getAttributes() + { + return this.normalAttrs; + } + + /** {@inheritDoc} */ + public void setAttributes(ShapeAttributes normalAttrs) + { + this.normalAttrs = normalAttrs; + this.updateModifiedTime(); + } + + /** {@inheritDoc} */ + public ShapeAttributes getHighlightAttributes() + { + return highlightAttrs; + } + + /** {@inheritDoc} */ + public void setHighlightAttributes(ShapeAttributes highlightAttrs) + { + this.highlightAttrs = highlightAttrs; + this.updateModifiedTime(); + } + + public String getPathType() + { + return this.pathType; + } + + public void setPathType(String pathType) + { + if (pathType == null) + { + String message = Logging.getMessage("nullValue.PathTypeIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.pathType = pathType; + this.onShapeChanged(); + } + + public double getTexelsPerEdgeInterval() + { + return this.texelsPerEdgeInterval; + } + + public void setTexelsPerEdgeInterval(double texelsPerEdgeInterval) + { + if (texelsPerEdgeInterval <= 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "texelsPerEdgeInterval <= 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.texelsPerEdgeInterval = texelsPerEdgeInterval; + this.onShapeChanged(); + } + + public int[] getMinAndMaxEdgeIntervals() + { + return new int[] {this.minEdgeIntervals, this.maxEdgeIntervals}; + } + + public void setMinAndMaxEdgeIntervals(int minEdgeIntervals, int maxEdgeIntervals) + { + if (minEdgeIntervals < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "minEdgeIntervals < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (maxEdgeIntervals < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "maxEdgeIntervals < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.minEdgeIntervals = minEdgeIntervals; + this.maxEdgeIntervals = maxEdgeIntervals; + this.onShapeChanged(); + } + + /** + * {@inheritDoc} + *

    + * The returned state key is constructed the SurfaceShape's unique ID, last modified time, and its active + * attributes. The returned state key has no dependency on the {@link gov.nasa.worldwind.globes.Globe}. Subclasses + * that depend on the Globe should return a state key that include the globe's state key. + */ + @Override + public Object getStateKey(DrawContext dc) + { + // Store a copy of the active attributes to insulate the key from changes made to the shape's active attributes. + // Use a null globe state key because SurfaceShape does not depend on the globe by default. + return new SurfaceShapeStateKey(this.getUniqueId(), this.lastModifiedTime, this.getActiveAttributes().copy(), + null); + } + + @SuppressWarnings({"unchecked"}) + public List getSectors(DrawContext dc) + { + if (dc == null) + { + String message = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + CacheEntry entry = this.sectorCache.get(dc.getGlobe()); + if (entry != null && entry.isValid(dc)) + { + return (List) entry.object; + } + else + { + entry = new CacheEntry(this.computeSectors(dc), dc); + this.sectorCache.put(dc.getGlobe(), entry); + return (List) entry.object; + } + } + + /** + * Computes the bounding sectors for the shape. There will be more than one if the shape crosses the date line, but + * does not enclose a pole. + * + * @param dc Current draw context. + * + * @return Bounding sectors for the shape. + */ + protected List computeSectors(DrawContext dc) + { + return this.computeSectors(dc.getGlobe()); + } + + /** + * Computes the bounding sectors for the shape. There will be more than one if the shape crosses the date line, but + * does not enclose a pole. + * + * @param globe Current globe. + * + * @return Bounding sectors for the shape. + */ + protected List computeSectors(Globe globe) + { + Iterable locations = this.getLocations(globe); + if (locations == null) + return null; + + List sectors = null; + + String pole = this.containsPole(locations); + if (pole != null) + { + // If the shape contains a pole, then the bounding sector is defined by the shape's extreme latitude, the + // latitude of the pole, and the full range of longitude. + Sector s = Sector.boundingSector(locations); + if (AVKey.NORTH.equals(pole)) + s = new Sector(s.minLatitude, Angle.POS90, Angle.NEG180, Angle.POS180); + else + s = new Sector(Angle.NEG90, s.maxLatitude, Angle.NEG180, Angle.POS180); + + sectors = Arrays.asList(s); + } + else if (LatLon.locationsCrossDateLine(locations)) + { + Sector[] array = Sector.splitBoundingSectors(locations); + if (array != null && array.length == 2 && !isSectorEmpty(array[0]) && !isSectorEmpty(array[1])) + sectors = Arrays.asList(array); + } + else + { + Sector s = Sector.boundingSector(locations); + if (!isSectorEmpty(s)) + sectors = Arrays.asList(s); + } + + if (sectors == null) + return null; + + // Great circle paths between two latitudes may result in a latitude which is greater or smaller than either of + // the two latitudes. All other path types are bounded by the defining locations. + if (AVKey.GREAT_CIRCLE.equals(this.getPathType())) + { + for (int i = 0; i < sectors.size(); i++) + { + Sector s = sectors.get(i); + + LatLon[] extremes = LatLon.greatCircleArcExtremeLocations(locations); + + double minLatDegrees = s.minLatitude.degrees; + double maxLatDegrees = s.maxLatitude.degrees; + + if (minLatDegrees > extremes[0].getLatitude().degrees) + minLatDegrees = extremes[0].getLatitude().degrees; + if (maxLatDegrees < extremes[1].getLatitude().degrees) + maxLatDegrees = extremes[1].getLatitude().degrees; + + Angle minLat = Angle.fromDegreesLatitude(minLatDegrees); + Angle maxLat = Angle.fromDegreesLatitude(maxLatDegrees); + + sectors.set(i, new Sector(minLat, maxLat, s.minLongitude, s.maxLongitude)); + } + } + + return sectors; + } + + protected static boolean isSectorEmpty(Sector sector) + { + if (sector == null) + return true; + + //noinspection SimplifiableIfStatement + if (sector.equals(Sector.EMPTY_SECTOR)) + return true; + + return sector.minLatitude.equals(sector.maxLatitude) + && sector.minLongitude.equals(sector.maxLongitude); + } + + /** + * Returns this SurfaceShape's enclosing volume as an {@link gov.nasa.worldwind.geom.Extent} in model coordinates, + * given a specified {@link gov.nasa.worldwind.globes.Globe} and vertical exaggeration (see {@link + * gov.nasa.worldwind.SceneController#getVerticalExaggeration()}. + * + * @param globe the Globe this SurfaceShape is related to. + * @param verticalExaggeration the vertical exaggeration of the scene containing this SurfaceShape. + * + * @return this SurfaceShape's Extent in model coordinates. + * + * @throws IllegalArgumentException if the Globe is null. + */ + public Extent getExtent(Globe globe, double verticalExaggeration) + { + if (globe == null) + { + String message = Logging.getMessage("nullValue.GlobeIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + List sectors = this.computeSectors(globe); + if (sectors == null) + return null; + + return this.computeExtent(globe, verticalExaggeration, sectors); + } + +// public String getRestorableState() +// { +// RestorableSupport rs = RestorableSupport.newRestorableSupport(); +// this.doGetRestorableState(rs, null); +// +// return rs.getStateAsXml(); +// } +// +// public void restoreState(String stateInXml) +// { +// if (stateInXml == null) +// { +// String message = Logging.getMessage("nullValue.StringIsNull"); +// Logging.error(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.error(message); +// throw new IllegalArgumentException(message, e); +// } +// +// this.doRestoreState(rs, null); +// } + + public double getArea(Globe globe) + { + if (globe == null) + { + String message = Logging.getMessage("nullValue.GlobeIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + AreaMeasurer areaMeasurer = this.setupAreaMeasurer(globe); + return areaMeasurer.getArea(globe); + } + + public double getArea(Globe globe, boolean terrainConformant) + { + if (globe == null) + { + String message = Logging.getMessage("nullValue.GlobeIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + AreaMeasurer areaMeasurer = this.setupAreaMeasurer(globe); + areaMeasurer.setFollowTerrain(terrainConformant); + return areaMeasurer.getArea(globe); + } + + public double getPerimeter(Globe globe) + { + if (globe == null) + { + String message = Logging.getMessage("nullValue.GlobeIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + AreaMeasurer areaMeasurer = this.setupAreaMeasurer(globe); + return areaMeasurer.getPerimeter(globe); + } + + public double getWidth(Globe globe) + { + if (globe == null) + { + String message = Logging.getMessage("nullValue.GlobeIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + AreaMeasurer areaMeasurer = this.setupAreaMeasurer(globe); + return areaMeasurer.getWidth(globe); + } + + public double getHeight(Globe globe) + { + if (globe == null) + { + String message = Logging.getMessage("nullValue.GlobeIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + AreaMeasurer areaMeasurer = this.setupAreaMeasurer(globe); + return areaMeasurer.getHeight(globe); + } + + public double getLength(Globe globe) + { + if (globe == null) + { + String message = Logging.getMessage("nullValue.GlobeIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + AreaMeasurer areaMeasurer = this.setupAreaMeasurer(globe); + return areaMeasurer.getLength(globe); + } + + public void move(Position position) + { + if (position == null) + { + String message = Logging.getMessage("nullValue.PositionIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + Position referencePosition = this.getReferencePosition(); + if (referencePosition == null) + return; + + this.moveTo(referencePosition.add(position)); + } + + public void moveTo(Position position) + { + if (position == null) + { + String message = Logging.getMessage("nullValue.PositionIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + Position oldReferencePosition = this.getReferencePosition(); + if (oldReferencePosition == null) + return; + + this.doMoveTo(oldReferencePosition, position); + } + + public abstract Position getReferencePosition(); + + protected abstract void doMoveTo(Position oldReferencePosition, Position newReferencePosition); + + protected void onShapeChanged() + { + this.updateModifiedTime(); + this.clearCaches(); + } + + /** + * {@inheritDoc} + *

    + * Overridden to clear this SurfaceShape's internal sector and geometry caches. + */ + @Override + protected void clearCaches() + { + super.clearCaches(); + this.sectorCache.clear(); + this.geometryCache.clear(); + } + + //**************************************************************// + //******************** Rendering *****************************// + //**************************************************************// + + /** + * Overridden to determine the shape's active attributes during preRendering, prior to building the shape's pickable + * representation and the SceneController's composite representation. + * + * @param dc the current draw context. + */ + @Override + protected void makeOrderedPreRenderable(DrawContext dc) + { + this.determineActiveAttributes(); + super.makeOrderedPreRenderable(dc); + } + + protected void drawGeographic(DrawContext dc, SurfaceTileDrawContext sdc) + { + if (dc == null) + { + String message = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (sdc == null) + { + String message = Logging.getMessage("nullValue.SurfaceTileDrawContextIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.beginDrawing(dc, sdc); + try + { + this.doDrawGeographic(dc, sdc); + } + finally + { + this.endDrawing(dc); + } + } + + protected void beginDrawing(DrawContext dc, SurfaceTileDrawContext sdc) + { + + this.stackHandler.pushAttrib( + GL_COLOR_BUFFER_BIT // For alpha test func and ref, blend func +// | GL_CURRENT_BIT // For current color. +// | GL_ENABLE_BIT // For disable depth test. +// | GL_LINE_BIT // For line width, line smooth, line stipple. + | GL_POLYGON_BIT ); // For cull enable and cull face. +// | GL_TRANSFORM_BIT); // For matrix mode. + + GpuProgram program = WWIO.getGpuProgram(dc.getGpuResourceCache(), R.raw.uniform_color_frag, R.raw.simple_vert, R.raw.uniform_color_frag); + program.bind(); + dc.setCurrentProgram(program); + program.enableVertexAttribute("vertexPoint"); + program.loadUniform1f("uOpacity", dc.isPickingMode() ? 1 : dc.getCurrentLayer().getOpacity()); + + // Disable the depth test. + glDisable(GL_DEPTH_TEST); + + // Enable backface culling. + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + + // Enable blending. + if (!dc.isPickingMode()) + { + glEnable(GL_BLEND); + } + + this.applyModelviewTransform(dc, sdc); + } + + protected void endDrawing(DrawContext dc) + { + if (texture != null && !dc.isPickingMode()) + { + glBindTexture(GL_TEXTURE_2D, 0); + } + + this.stackHandler.popAttrib(); + + GpuProgram program = dc.getCurrentProgram(); + if (program == null) return; // Message already logged in getDefaultGpuProgram via beginDrawing. + + // Disable the program's vertexPoint attribute, if one exists. This restores the program state modified in + // beginRendering. This must be done while the program is still bound, because getAttribLocation depends on + // the current OpenGL program state. + program.disableVertexAttribute("vertexPoint"); + + // Restore the previous OpenGL program state. + dc.setCurrentProgram(null); + GLES20.glUseProgram(0); + WorldWindowImpl.glCheckError("glUseProgram"); + + // Restore the OpenGL array and element array buffer bindings to 0. + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + WorldWindowImpl.glCheckError("glBindBuffer"); + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0); + WorldWindowImpl.glCheckError("glBindBuffer"); + } + + protected void doDrawGeographic(DrawContext dc, SurfaceTileDrawContext sdc) + { + this.determineActiveGeometry(dc, sdc); + + if (this.getActiveAttributes(). isEnableInterior() && this.getActiveAttributes().getInteriorOpacity() > 0) + this.drawInterior(dc, sdc); + + if (this.getActiveAttributes().isEnableOutline() && this.getActiveAttributes().getOutlineOpacity() > 0) + this.drawOutline(dc, sdc); + } + + protected void applyModelviewTransform(DrawContext dc, SurfaceTileDrawContext sdc) + { + // Apply the geographic to surface tile coordinate transform. + Matrix modelview = sdc.getModelviewMatrix(); + + // If the SurfaceShape has a non-null reference position, transform to the local coordinate system that has its + // origin at the reference position. + Position refPos = this.getReferencePosition(); + if (refPos != null) + { + Matrix refMatrix = Matrix.fromTranslation(refPos.getLongitude().degrees, refPos.getLatitude().degrees, 0); + modelview = modelview.multiply(refMatrix); + } + Matrix mvp = Matrix.fromIdentity(); + mvp.multiplyAndSet(sdc.getProjectionMatrix(), modelview); + dc.getCurrentProgram().loadUniformMatrix("mvpMatrix", mvp); + } + + /** Determines which attributes -- normal, highlight or default -- to use each frame. */ + protected void determineActiveAttributes() + { + if (this.isHighlighted()) + { + if (this.getHighlightAttributes() != null) + this.activeAttrs.set(this.getHighlightAttributes()); + else + { + // If no highlight attributes have been specified we need to use the normal attributes but adjust them + // to cause highlighting. + if (this.getAttributes() != null) + this.activeAttrs.set(this.getAttributes()); + + this.activeAttrs.setOutlineMaterial(DEFAULT_HIGHLIGHT_MATERIAL); + this.activeAttrs.setInteriorMaterial(DEFAULT_HIGHLIGHT_MATERIAL); + this.activeAttrs.setOutlineOpacity(1); + this.activeAttrs.setInteriorOpacity(1); + } + } + else if (this.getAttributes() != null) + { + this.activeAttrs.set(this.getAttributes()); + } + else + { + this.activeAttrs.set(defaultAttrs); + } + } + + protected ShapeAttributes createActiveAttributes() + { + return new BasicShapeAttributes(); + } + + protected ShapeAttributes getActiveAttributes() + { + return this.activeAttrs; + } + + protected void determineActiveGeometry(DrawContext dc, SurfaceTileDrawContext sdc) + { + this.activeGeometry.clear(); + this.activeOutlineGeometry.clear(); + + List> geom = this.getCachedGeometry(dc, sdc); + if (geom == null) + return; + + for (List locations : geom) + { + List drawLocations = new ArrayList(locations); + + String pole = this.containsPole(drawLocations); + if (pole != null) + { + // Wrap the shape interior around the pole and along the anti-meridian. See WWJ-284. + List poleLocations = this.cutAlongDateLine(drawLocations, pole, dc.getGlobe()); + this.activeGeometry.add(poleLocations); + // The outline need only compensate for dateline crossing. See WWJ-452. + List> datelineLocations = this.repeatAroundDateline(drawLocations); + this.activeOutlineGeometry.addAll(datelineLocations); + } + else if (LatLon.locationsCrossDateLine(drawLocations)) + { + List> datelineLocations = this.repeatAroundDateline(drawLocations); + this.activeGeometry.addAll(datelineLocations); + this.activeOutlineGeometry.addAll(datelineLocations); + } + else + { + this.activeGeometry.add(drawLocations); + this.activeOutlineGeometry.add(drawLocations); + } + } + } + + /** + * Indicates whether the shape is a closed polygon that can enclose a pole, or an open path that cannot. This makes + * a difference when computing the bounding sector for a shape. For example, consider the positions (-100, 85), (0, + * 80), (100, 80). If these positions are treated as a closed polygon (a triangle over the North Pole) then the + * bounding sector is 80 to 90 lat, -180 to 180 lon. But if they are treated as an open path (a line wrapping + * partway around the pole) then the bounding sector is 80 to 85 lat, -100 to 100 lon. + * + * @return True if the shape is a closed polygon that can contain a pole, or false if it is treated as an open path + * that cannot contain a pole. + */ + protected boolean canContainPole() + { + return true; + } + + /** + * Determine if a list of geographic locations encloses either the North or South pole. The list is treated as a + * closed loop. (If the first and last positions are not equal the loop will be closed for purposes of this + * computation.) + * + * @param locations Locations to test. + * + * @return AVKey.NORTH if the North Pole is enclosed, AVKey.SOUTH if the South Pole is enclosed, or null if neither + * pole is enclosed. Always returns null if {@link #canContainPole()} returns false. + */ + // TODO handle a shape that contains both poles. + protected String containsPole(Iterable locations) + { + if (!this.canContainPole()) + return null; + + // Determine how many times the path crosses the date line. Shapes that include a pole will cross an odd number of times. + boolean containsPole = false; + + double minLatitude = 90.0; + double maxLatitude = -90.0; + + LatLon first = null; + LatLon prev = null; + for (LatLon ll : locations) + { + if (first == null) + first = ll; + + if (prev != null && LatLon.locationsCrossDateline(prev, ll)) + containsPole = !containsPole; + + if (ll.latitude.degrees < minLatitude) + minLatitude = ll.latitude.degrees; + + if (ll.latitude.degrees > maxLatitude) + maxLatitude = ll.latitude.degrees; + + prev = ll; + } + + // Close the loop by connecting the last position to the first. If the loop is already closed then the following + // test will always fail, and will not affect the result. + if (first != null && LatLon.locationsCrossDateline(first, prev)) + containsPole = !containsPole; + + if (!containsPole) + return null; + + // Determine which pole is enclosed. If the shape is entirely in one hemisphere, then assume that it encloses + // the pole in that hemisphere. Otherwise, assume that it encloses the pole that is closest to the shape's + // extreme latitude. + if (minLatitude > 0) + return AVKey.NORTH; // Entirely in Northern Hemisphere + else if (maxLatitude < 0) + return AVKey.SOUTH; // Entirely in Southern Hemisphere + else if (Math.abs(maxLatitude) >= Math.abs(minLatitude)) + return AVKey.NORTH; // Spans equator, but more north than south + else + return AVKey.SOUTH; + } + + /** + * Divide a list of locations that encloses a pole along the international date line. This method determines where + * the locations cross the date line, and inserts locations to the pole, and then back to the intersection position. + * This allows the shape to be "unrolled" when projected in a lat-lon projection. + * + * @param locations Locations to cut at date line. This list is not modified. + * @param pole Pole contained by locations, either AVKey.NORTH or AVKey.SOUTH. + * @param globe Current globe. + * + * @return New location list with locations added to correctly handle date line intersection. + */ + protected List cutAlongDateLine(List locations, String pole, Globe globe) + { + // If the locations do not contain a pole, then there's nothing to do. + if (pole == null) + return locations; + + List newLocations = new ArrayList(locations.size()); + + Angle poleLat = AVKey.NORTH.equals(pole) ? Angle.POS90 : Angle.NEG90; + + LatLon pos = null; + for (LatLon posNext : locations) + { + if (pos != null) + { + newLocations.add(pos); + if (LatLon.locationsCrossDateline(pos, posNext)) + { + // Determine where the segment crosses the date line. + LatLon separation = this.intersectionWithMeridian(pos, posNext, Angle.POS180, globe); + double sign = Math.signum(pos.getLongitude().degrees); + + Angle lat = separation.getLatitude(); + Angle thisSideLon = Angle.POS180.multiply(sign); + Angle otherSideLon = thisSideLon.multiply(-1); + + // Add locations that run from the intersection to the pole, then back to the intersection. Note + // that the longitude changes sign when the path returns from the pole. + // . Pole + // 2 ^ | 3 + // | | + // 1 | v 4 + // --->---- ------> + newLocations.add(new LatLon(lat, thisSideLon)); + newLocations.add(new LatLon(poleLat, thisSideLon)); + newLocations.add(new LatLon(poleLat, otherSideLon)); + newLocations.add(new LatLon(lat, otherSideLon)); + } + } + pos = posNext; + } + newLocations.add(pos); + + return newLocations; + } + + /** + * Returns a list containing two copies of the specified list of locations crossing the dateline: one that extends + * across the -180 longitude boundary and one that extends across the +180 longitude boundary. If the list of + * locations does not cross the dateline this returns a list containing a copy of the original list. + * + * @param locations Locations to repeat. This is list not modified. + * + * @return A list containing two new location lists, one copy for either side of the date line. + */ + protected List> repeatAroundDateline(List locations) + { + List> list = new ArrayList>(); + + LatLon prev = null; + double lonOffset = 0; + boolean applyLonOffset = false; + + List locationsA = new ArrayList(locations.size()); + list.add(locationsA); + + for (LatLon cur : locations) + { + if (prev != null && LatLon.locationsCrossDateline(prev, cur)) + { + if (lonOffset == 0) + lonOffset = (prev.longitude.degrees < 0 ? -360 : 360); + + applyLonOffset = !applyLonOffset; + } + + if (applyLonOffset) + { + locationsA.add(LatLon.fromDegrees(cur.latitude.degrees, cur.longitude.degrees + lonOffset)); + } + else + { + locationsA.add(cur); + } + + prev = cur; + } + + if (lonOffset != 0) // longitude offset is non-zero when the locations cross the dateline + { + List locationsB = new ArrayList(locations.size()); + list.add(locationsB); + + for (LatLon cur : locationsA) + { + locationsB.add(LatLon.fromDegrees(cur.latitude.degrees, cur.longitude.degrees - lonOffset)); + } + } + + return list; + } + + /** + * Determine where a line between two positions crosses a given meridian. The intersection test is performed by + * intersecting a line in Cartesian space between the two positions with a plane through the meridian. Thus, it is + * most suitable for working with positions that are fairly close together as the calculation does not take into + * account great circle or rhumb paths. + * + * @param p1 First position. + * @param p2 Second position. + * @param meridian Longitude line to intersect with. + * @param globe Globe used to compute intersection. + * + * @return The intersection location along the meridian + */ + protected LatLon intersectionWithMeridian(LatLon p1, LatLon p2, Angle meridian, Globe globe) + { + Vec4 pt1 = globe.computePointFromLocation(p1); + Vec4 pt2 = globe.computePointFromLocation(p2); + + // Compute a plane through the origin, North Pole, and the desired meridian. + Vec4 northPole = globe.computePointFromLocation(new LatLon(Angle.POS90, meridian)); + Vec4 pointOnEquator = globe.computePointFromLocation(new LatLon(Angle.ZERO, meridian)); + + Plane plane = Plane.fromPoints(northPole, pointOnEquator, Vec4.ZERO); + + Vec4 intersectionPoint = plane.intersect(Line.fromSegment(pt1, pt2)); + if (intersectionPoint == null) + return null; + + Position intersectionPos = globe.computePositionFromPoint(intersectionPoint); + + return new LatLon(intersectionPos.getLatitude(), meridian); + } + + protected List> getActiveGeometry() + { + return this.activeGeometry; + } + + protected void drawInterior(DrawContext dc, SurfaceTileDrawContext sdc) + { + if (this.getActiveGeometry().isEmpty()) + return; + + this.applyInteriorState(dc, sdc, this.getActiveAttributes(), this.getInteriorTexture(), + this.getReferencePosition()); + + // Tessellate and draw the interior, making no assumptions about the nature or structure of the shape's + // vertices. The interior is treated as a potentially complex polygon, and this code will do its best to + // rasterize that polygon. The outline is treated as a simple line loop, regardless of whether the shape's + // vertices actually define a closed path. + this.tessellateInterior(dc); + } + + protected void drawOutline(DrawContext dc, SurfaceTileDrawContext sdc) + { + if (this.activeOutlineGeometry.isEmpty()) + return; + + Position refPos = this.getReferencePosition(); + if (refPos == null) + return; + + this.applyOutlineState(dc, this.getActiveAttributes()); + + for (List drawLocations : this.activeOutlineGeometry) + { + if (vertexBuffer == null || vertexBuffer.capacity() < 2 * drawLocations.size()) + vertexBuffer = BufferUtil.newFloatBuffer(2 * drawLocations.size()); + vertexBuffer.clear(); + + for (LatLon ll : drawLocations) + { + vertexBuffer.put((float) (ll.getLongitude().degrees - refPos.getLongitude().degrees)); + vertexBuffer.put((float) (ll.getLatitude().degrees - refPos.getLatitude().degrees)); + } + vertexBuffer.flip(); + + dc.getCurrentProgram().vertexAttribPointer("vertexPoint", 2, GL_FLOAT, false, 0, vertexBuffer); + glDrawArrays(GL_LINE_STRIP, 0, drawLocations.size()); + } + } + + protected GpuTexture getInteriorTexture() + { + if (this.getActiveAttributes().getImageSource() == null) + { + this.texture = null; + } + else if (this.texture == null) + { + GpuTextureData data = GpuTextureData.createTextureData(getActiveAttributes().getImageSource(), null, false); + this.texture = GpuTexture.createTexture(data); + } + + return this.texture; + } + + @SuppressWarnings({"unchecked"}) + protected List> getCachedGeometry(DrawContext dc, SurfaceTileDrawContext sdc) + { + if (dc == null) + { + String message = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + Object key = this.createGeometryKey(dc, sdc); + CacheEntry entry = this.geometryCache.get(key); + if (entry != null && entry.isValid(dc)) + { + return (List>) entry.object; + } + else + { + entry = new CacheEntry(this.createGeometry(dc.getGlobe(), sdc), dc); + this.geometryCache.put(key, entry); + return (List>) entry.object; + } + } + + protected abstract List> createGeometry(Globe globe, SurfaceTileDrawContext sdc); + + protected Object createGeometryKey(DrawContext dc, SurfaceTileDrawContext sdc) + { + return new GeometryKey(dc, this.computeEdgeIntervalsPerDegree(sdc)); + } + + protected double computeEdgeIntervalsPerDegree(SurfaceTileDrawContext sdc) + { + double texelsPerDegree = Math.max( + sdc.getViewport().width() / sdc.getSector().getDeltaLonDegrees(), + sdc.getViewport().height() / sdc.getSector().getDeltaLatDegrees()); + double intervalsPerTexel = 1.0 / this.getTexelsPerEdgeInterval(); + + return intervalsPerTexel * texelsPerDegree; + } + + //**************************************************************// + //******************** Rendering State ***********************// + //**************************************************************// + + protected void applyInteriorState(DrawContext dc, SurfaceTileDrawContext sdc, ShapeAttributes attributes, + GpuTexture texture, LatLon refLocation) + { +// if (texture != null && !dc.isPickingMode()) +// { +// this.applyInteriorTextureState(dc, sdc, attributes, texture, refLocation); +// } +// else +// { + if (!dc.isPickingMode()) + { + // Apply blending in non-premultiplied color mode. + OGLUtil.applyBlending(false); + + // Set the current RGBA color to the interior color and opacity. + Color current = new Color(attributes.getInteriorMaterial().getDiffuse()); + current.a = current.a*attributes.getInteriorOpacity(); + dc.getCurrentProgram().loadUniformColor("uColor", current); + } +// } + } + + protected void applyOutlineState(DrawContext dc, ShapeAttributes attributes) + { + // Apply line width state + double lineWidth = attributes.getOutlineWidth(); + if (dc.isPickingMode() && !attributes.isEnableInterior()) + { + if (lineWidth != 0) + lineWidth += 5; + } + glLineWidth((float) lineWidth); + + if (!dc.isPickingMode()) + { + // Apply blending in non-premultiplied color mode. + OGLUtil.applyBlending(false); + // Set the current RGBA color to the outline color and opacity. Convert the floating point opacity from the + // range [0, 1] to the unsigned byte range [0, 255]. + Color current = new Color(attributes.getOutlineMaterial().getDiffuse()); + current.a = current.a*attributes.getOutlineOpacity(); + dc.getCurrentProgram().loadUniformColor("uColor", current); + } + } + + protected void applyInteriorTextureState(DrawContext dc, SurfaceTileDrawContext sdc, ShapeAttributes attributes, + GpuTexture texture, LatLon refLocation) + { + texture.bind(); + + if (!dc.isPickingMode()) + { + // Apply blending in premultiplied color mode, and set the current RGBA color to white, with the specified + // opacity. + OGLUtil.applyBlending(true); + dc.getCurrentProgram().loadUniformColor("uColor", new Color(1f, 1f, 1f, attributes.getInteriorOpacity())); + } + + // Apply texture transform. + Matrix transform = Matrix.IDENTITY; + // Translate geographic coordinates to the reference location. + if (refLocation != null) + { + double refLatDegrees = refLocation.getLatitude().degrees; + double refLonDegrees = refLocation.getLongitude().degrees; + transform = Matrix.fromTranslation(refLonDegrees, refLatDegrees, 0d).multiply(transform); + } + // Premultiply pattern scaling and cos latitude to compensate latitude distortion on x + double cosLat = refLocation != null ? refLocation.getLatitude().cos() : 1d; + double scale = attributes.getImageScale(); + transform = Matrix.fromScale(cosLat / scale, 1d / scale, 1d).multiply(transform); + // To maintain the pattern apparent size, we scale it so that one texture pixel match one draw tile pixel. + double regionPixelSize = dc.getGlobe().getRadius() * sdc.getSector().getDeltaLatRadians() + / sdc.getViewport().height(); + double texturePixelSize = dc.getGlobe().getRadius() * Angle.fromDegrees(1).radians / texture.getHeight(); + double drawScale = texturePixelSize / regionPixelSize; + transform = Matrix.fromScale(drawScale, drawScale, 1d).multiply(transform); // Pre multiply + + Matrix tex = Matrix.fromIdentity(); + // Apply texture coordinates transform + texture.applyInternalTransform(dc, tex); + tex.multiplyAndSet(transform); + + // Apply texture environment and parameters. + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + } + + //**************************************************************// + //******************** Intermediate Locations ****************// + //**************************************************************// + + protected void generateIntermediateLocations(Iterable iterable, double edgeIntervalsPerDegree, + boolean makeClosedPath, List locations) + { + LatLon firstLocation = null; + LatLon lastLocation = null; + + for (LatLon ll : iterable) + { + if (firstLocation == null) + { + firstLocation = ll; + } + + if (lastLocation != null) + { + this.addIntermediateLocations(lastLocation, ll, edgeIntervalsPerDegree, locations); + } + + locations.add(ll); + lastLocation = ll; + } + + // If the caller has instructed us to generate locations for a closed path, then check to see if the specified + // locations define a closed path. If not, then we need to generate intermediate locations between the last + // and first locations, then close the path by repeating the first location. + if (makeClosedPath) + { + if (firstLocation != null && lastLocation != null && !firstLocation.equals(lastLocation)) + { + this.addIntermediateLocations(lastLocation, firstLocation, edgeIntervalsPerDegree, locations); + locations.add(firstLocation); + } + } + } + + @SuppressWarnings({"StringEquality"}) + protected void addIntermediateLocations(LatLon a, LatLon b, double edgeIntervalsPerDegree, List locations) + { + if (this.pathType != null && this.pathType == AVKey.GREAT_CIRCLE) + { + Angle pathLength = LatLon.greatCircleDistance(a, b); + + double edgeIntervals = WWMath.clamp(edgeIntervalsPerDegree * pathLength.degrees, + this.minEdgeIntervals, this.maxEdgeIntervals); + int numEdgeIntervals = (int) Math.ceil(edgeIntervals); + + if (numEdgeIntervals > 1) + { + double headingRadians = LatLon.greatCircleAzimuth(a, b).radians; + double stepSizeRadians = pathLength.radians / (numEdgeIntervals + 1); + + for (int i = 1; i <= numEdgeIntervals; i++) + { + locations.add(LatLon.greatCircleEndPosition(a, headingRadians, i * stepSizeRadians)); + } + } + } + else if (this.pathType != null && (this.pathType == AVKey.RHUMB_LINE || this.pathType == AVKey.LOXODROME)) + { + Angle pathLength = LatLon.rhumbDistance(a, b); + + double edgeIntervals = WWMath.clamp(edgeIntervalsPerDegree * pathLength.degrees, + this.minEdgeIntervals, this.maxEdgeIntervals); + int numEdgeIntervals = (int) Math.ceil(edgeIntervals); + + if (numEdgeIntervals > 1) + { + double headingRadians = LatLon.rhumbAzimuth(a, b).radians; + double stepSizeRadians = pathLength.radians / (numEdgeIntervals + 1); + + for (int i = 1; i <= numEdgeIntervals; i++) + { + locations.add(LatLon.rhumbEndPosition(a, headingRadians, i * stepSizeRadians)); + } + } + } + else // Default to linear interpolation in latitude and longitude. + { + // Linear interpolation between 2D coordinates is already performed by GL during shape rasterization. + // There is no need to duplicate that effort here. + } + } + + //**************************************************************// + //******************** Interior Tessellation *****************// + //**************************************************************// + + protected Integer tessellateInterior(DrawContext dc) + { + if (dc == null) + { + String message = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + try + { + return this.doTessellateInterior(dc); + } + catch (OutOfMemoryError e) + { + String message = Logging.getMessage("generic.ExceptionWhileTessellating", this); + Logging.error( message, e); + + //noinspection ThrowableInstanceNeverThrown +// throw new WWRuntimeException(message, e); + this.handleUnsuccessfulInteriorTessellation(dc); + return null; + } + } + + protected Integer doTessellateInterior(DrawContext dc) { + int numBytes = 0; + + Position referencePos = this.getReferencePosition(); + if (referencePos == null) + return null; + + for (List drawLocations : this.getActiveGeometry()) + { + if (vertexBuffer == null || vertexBuffer.capacity() < 2 * drawLocations.size()) + vertexBuffer = BufferUtil.newFloatBuffer(2 * (drawLocations.size()+2)); + vertexBuffer.clear(); + + LatLon firstPoint = drawLocations.get(0); + + //calculate center and draw triangle fan all around it + LatLon centerPoint = LatLon.interpolate(.5f, firstPoint, drawLocations.get(drawLocations.size()/2)); + + vertexBuffer.put((float) (centerPoint.getLongitude().degrees - referencePos.getLongitude().degrees)); + vertexBuffer.put((float) (centerPoint.getLatitude().degrees - referencePos.getLatitude().degrees)); + numBytes += 2 * 8; // 3 coords of 8 bytes each + + for (LatLon ll : drawLocations) + { + vertexBuffer.put((float) (ll.getLongitude().degrees - referencePos.getLongitude().degrees)); + vertexBuffer.put((float) (ll.getLatitude().degrees - referencePos.getLatitude().degrees)); + numBytes += 2 * 8; // 3 coords of 8 bytes each + } + //Add first point again + vertexBuffer.put((float) (firstPoint.getLongitude().degrees - referencePos.getLongitude().degrees)); + vertexBuffer.put((float) (firstPoint.getLatitude().degrees - referencePos.getLatitude().degrees)); + numBytes += 2 * 8; // 3 coords of 8 bytes each + vertexBuffer.flip(); + + dc.getCurrentProgram().vertexAttribPointer("vertexPoint", 2, GL_FLOAT, false, 0, vertexBuffer); + glDrawArrays(GL_TRIANGLE_FAN, 0, drawLocations.size()); + } + + return numBytes; + } + +// protected Integer doTessellateInterior(DrawContext dc) +// { +// +// GLUtessellatorCallback cb = GLUTessellatorSupport.createOGLDrawPrimitivesCallback(gl); +// +// // Create a tessellator with the default winding rule: GLU_TESS_WINDING_ODD. This winding rule produces the +// // expected tessellation when the shape's contours all have a counter-clockwise winding. +// GLUTessellatorSupport glts = new GLUTessellatorSupport(); +// glts.beginTessellation(cb, new Vec4(0, 0, 1)); +// try +// { +// return this.tessellateInteriorVertices(glts.getGLUtessellator()); +// } +// finally +// { +// // Free any heap memory used for tessellation immediately. If tessellation has consumed all available heap +// // memory, we must free memory used by tessellation immediately or subsequent operations such as message +// // logging will fail. +// glts.endTessellation(); +// } +// } + +// protected Integer tessellateInteriorVertices(GLUtessellator tess) +// { +// if (this.getActiveGeometry().isEmpty()) +// return null; +// +// Position referencePos = this.getReferencePosition(); +// if (referencePos == null) +// return null; +// +// int numBytes = 0; +// GLU.gluTessBeginPolygon(tess, null); +// +// for (List drawLocations : this.getActiveGeometry()) +// { +// GLU.gluTessBeginContour(tess); +// for (LatLon ll : drawLocations) +// { +// double[] vertex = new double[3]; +// vertex[0] = ll.getLongitude().degrees - referencePos.getLongitude().degrees; +// vertex[1] = ll.getLatitude().degrees - referencePos.getLatitude().degrees; +// GLU.gluTessVertex(tess, vertex, 0, vertex); +// numBytes += 3 * 8; // 3 coords of 8 bytes each +// } +// GLU.gluTessEndContour(tess); +// } +// +// GLU.gluTessEndPolygon(tess); +// +// return numBytes; +// } + + protected void handleUnsuccessfulInteriorTessellation(DrawContext dc) + { + } + + //**************************************************************// + //******************** Measurement ***************************// + //**************************************************************// + + protected AreaMeasurer setupAreaMeasurer(Globe globe) + { + if (globe == null) + { + String message = Logging.getMessage("nullValue.GlobeIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (this.areaMeasurer == null) + { + this.areaMeasurer = new AreaMeasurer(); + } + + // Try to use the currently cached locations. If the AreaMeasurer is out of sync with this shape's state, + // then update the AreaMeasurer's internal location list. + if (this.areaMeasurerLastModifiedTime < this.lastModifiedTime) + { + // The AreaMeasurer requires an ArrayList reference, but SurfaceShapes use an opaque iterable. Copy the + // iterable contents into an ArrayList to satisfy AreaMeasurer without compromising the generality of the + // shape's iterator. + ArrayList arrayList = new ArrayList(); + + Iterable locations = this.getLocations(globe); + if (locations != null) + { + for (LatLon ll : locations) + { + arrayList.add(ll); + } + + if (arrayList.size() > 1 && !arrayList.get(0).equals(arrayList.get(arrayList.size() - 1))) + arrayList.add(arrayList.get(0)); + } + + this.areaMeasurer.setPositions(arrayList, 0); + this.areaMeasurerLastModifiedTime = this.lastModifiedTime; + } + + // Surface shapes follow the terrain by definition. + this.areaMeasurer.setFollowTerrain(true); + + return this.areaMeasurer; + } + + //**************************************************************// + //******************** Restorable State ***********************// + //**************************************************************// + +// protected void doGetRestorableState(RestorableSupport rs, RestorableSupport.StateObject context) +// { +// // Note: drawBoundingSectors is a diagnostic flag, therefore it is not saved or restored. +// +// rs.addStateValueAsBoolean(context, "visible", this.isVisible()); +// rs.addStateValueAsBoolean(context, "highlighted", this.isHighlighted()); +// rs.addStateValueAsString(context, "pathType", this.getPathType()); +// rs.addStateValueAsDouble(context, "texelsPerEdgeInterval", this.getTexelsPerEdgeInterval()); +// +// int[] minAndMaxEdgeIntervals = this.getMinAndMaxEdgeIntervals(); +// rs.addStateValueAsInteger(context, "minEdgeIntervals", minAndMaxEdgeIntervals[0]); +// rs.addStateValueAsInteger(context, "maxEdgeIntervals", minAndMaxEdgeIntervals[1]); +// +// if (this.getAttributes() != null) +// this.getAttributes().getRestorableState(rs, rs.addStateObject(context, "attributes")); +// +// if (this.getHighlightAttributes() != null) +// this.getHighlightAttributes().getRestorableState(rs, rs.addStateObject(context, "highlightAttrs")); +// +// RestorableSupport.StateObject so = rs.addStateObject(null, "avlist"); +// for (Map.Entry avp : this.getEntries()) +// { +// this.getRestorableStateForAVPair(avp.getKey(), avp.getValue() != null ? avp.getValue() : "", rs, so); +// } +// } +// +// protected void doRestoreState(RestorableSupport rs, RestorableSupport.StateObject context) +// { +// // Invoke the legacy restore functionality. This will enable the shape to recognize state XML elements +// // from the previous version of SurfaceShape. +// this.legacyRestoreState(rs, context); +// +// // Note: drawBoundingSectors is a diagnostic flag, therefore it is not saved or restored. +// +// Boolean b = rs.getStateValueAsBoolean(context, "visible"); +// if (b != null) +// this.setVisible(b); +// +// b = rs.getStateValueAsBoolean(context, "highlighted"); +// if (b != null) +// this.setHighlighted(b); +// +// String s = rs.getStateValueAsString(context, "pathType"); +// if (s != null) +// { +// String pathType = this.pathTypeFromString(s); +// if (pathType != null) +// this.setPathType(pathType); +// } +// +// Double d = rs.getStateValueAsDouble(context, "texelsPerEdgeInterval"); +// if (d != null) +// this.setTexelsPerEdgeInterval(d); +// +// int[] minAndMaxEdgeIntervals = this.getMinAndMaxEdgeIntervals(); +// +// Integer minEdgeIntervals = rs.getStateValueAsInteger(context, "minEdgeIntervals"); +// if (minEdgeIntervals != null) +// minAndMaxEdgeIntervals[0] = minEdgeIntervals; +// +// Integer maxEdgeIntervals = rs.getStateValueAsInteger(context, "maxEdgeIntervals"); +// if (maxEdgeIntervals != null) +// minAndMaxEdgeIntervals[1] = maxEdgeIntervals; +// +// if (minEdgeIntervals != null || maxEdgeIntervals != null) +// this.setMinAndMaxEdgeIntervals(minAndMaxEdgeIntervals[0], minAndMaxEdgeIntervals[1]); +// +// RestorableSupport.StateObject so = rs.getStateObject(context, "attributes"); +// if (so != null) +// { +// ShapeAttributes attrs = (this.getAttributes() != null) ? this.getAttributes() : new BasicShapeAttributes(); +// attrs.restoreState(rs, so); +// this.setAttributes(attrs); +// } +// +// so = rs.getStateObject(context, "highlightAttrs"); +// if (so != null) +// { +// ShapeAttributes attrs = (this.getHighlightAttributes() != null) ? this.getHighlightAttributes() +// : new BasicShapeAttributes(); +// attrs.restoreState(rs, so); +// this.setHighlightAttributes(attrs); +// } +// +// so = rs.getStateObject(null, "avlist"); +// if (so != null) +// { +// RestorableSupport.StateObject[] avpairs = rs.getAllStateObjects(so, ""); +// if (avpairs != null) +// { +// for (RestorableSupport.StateObject avp : avpairs) +// { +// if (avp != null) +// this.setValue(avp.getName(), avp.getValue()); +// } +// } +// } +// +// // We've potentially modified the shapes attributes in either legacyRestoreState(), or in +// // attributes.restoreState(). Flag that the shape has changed in order to ensure that any cached data associated +// // with the shape is invalidated. +// this.onShapeChanged(); +// } +// +// /** +// * Restores state values from previous versions of the SurfaceShape state XML. These values are stored or named +// * differently than the current implementation. Those values which have not changed are ignored here, and will +// * restored in {@link #doRestoreState(gov.nasa.worldwind.util.RestorableSupport, +// * gov.nasa.worldwind.util.RestorableSupport.StateObject)}. +// * +// * @param rs RestorableSupport object which contains the state value properties. +// * @param context active context in the RestorableSupport to read state from. +// */ +// protected void legacyRestoreState(RestorableSupport rs, RestorableSupport.StateObject context) +// { +// // Ignore texture width and height parameters, they're no longer used. +// +// //Integer width = rs.getStateValueAsInteger(context, "textureWidth"); +// //Integer height = rs.getStateValueAsInteger(context, "textureHeight"); +// //if (width != null && height != null) +// // this.setTextureSize(new Dimension(width, height)); +// +// ShapeAttributes attrs = this.getAttributes(); +// +// Color color = rs.getStateValueAsColor(context, "color"); +// if (color != null) +// (attrs != null ? attrs : (attrs = new BasicShapeAttributes())).setInteriorMaterial(new Material(color)); +// +// color = rs.getStateValueAsColor(context, "borderColor"); +// if (color != null) +// (attrs != null ? attrs : (attrs = new BasicShapeAttributes())).setOutlineMaterial(new Material(color)); +// +// Double dub = rs.getStateValueAsDouble(context, "lineWidth"); +// if (dub != null) +// (attrs != null ? attrs : (attrs = new BasicShapeAttributes())).setOutlineWidth(dub); +// +// // Ignore numEdgeIntervalsPerDegree, since it's no longer used. +// //Double intervals = rs.getStateValueAsDouble(context, "numEdgeIntervalsPerDegree"); +// //if (intervals != null) +// // this.setEdgeIntervalsPerDegree(intervals.intValue()); +// +// Boolean booleanState = rs.getStateValueAsBoolean(context, "drawBorder"); +// if (booleanState != null) +// (attrs != null ? attrs : (attrs = new BasicShapeAttributes())).setDrawOutline(booleanState); +// +// booleanState = rs.getStateValueAsBoolean(context, "drawInterior"); +// if (booleanState != null) +// (attrs != null ? attrs : (attrs = new BasicShapeAttributes())).setDrawInterior(booleanState); +// +// booleanState = rs.getStateValueAsBoolean(context, "antialias"); +// if (booleanState != null) +// (attrs != null ? attrs : (attrs = new BasicShapeAttributes())).setEnableAntialiasing(booleanState); +// +// if (attrs != null) +// this.setAttributes(attrs); +// +// // Positions data is a per object property now. This value is recognized by SurfacePolygon, SurfacePolyline, and +// // SurfaceSector. Other shapes ignore this property. +// +// //ArrayList locations = rs.getStateValueAsLatLonList(context, "locations"); +// //if (locations != null) +// // this.positions = locations; +// } + + protected String pathTypeFromString(String s) + { + if (s == null) + return null; + + if (s.equals(AVKey.GREAT_CIRCLE)) + { + return AVKey.GREAT_CIRCLE; + } + else if (s.equals(AVKey.LINEAR)) + { + return AVKey.LINEAR; + } + else if (s.equals(AVKey.LOXODROME)) + { + return AVKey.LOXODROME; + } + else if (s.equals(AVKey.RHUMB_LINE)) + { + return AVKey.RHUMB_LINE; + } + + return null; + } + + //**************************************************************// + //******************** State Key *****************************// + //**************************************************************// + + /** + * Represents a surface shapes's current state. SurfaceShapeStateKey extends {@link + * gov.nasa.worldwind.render.AbstractSurfaceObject.SurfaceObjectStateKey} by adding the shape's current {@link + * ShapeAttributes} and the globe's state key. + *

    + * SurfaceShapeStateKey uniquely identifies a surface shapes's current state exactly as SurfaceObjectStateKey does, + * but also distinguishes the shape's active ShapeAttributes from any previous attributes, and distinguishes between + * different globes via the globe state key. + */ + protected static class SurfaceShapeStateKey extends SurfaceObjectStateKey + { + /** The SurfaceShape's attributes. May be null if the shape has no attributes. */ + protected final ShapeAttributes attributes; + /** The Globe's state key. May be null if the shape's state does not depend on the globe. */ + protected final Object globeStateKey; + + /** + * Constructs a new SurfaceShapeStateKey with the specified unique ID, modified time, attributes, and globe + * state key. The globe state key should be null if the surface shape does not depend on the globe. + * + * @param uniqueID the SurfaceShape's unique ID. + * @param modifiedTime the SurfaceShape's modified time. + * @param attributes the SurfaceShape's attributes, or null if the shape has no attributes. + * @param globeStateKey the globe's state key, or null if the shape does not depend on the globe. + * + * @see gov.nasa.worldwind.globes.Globe#getStateKey(gov.nasa.worldwind.render.DrawContext) + */ + public SurfaceShapeStateKey(long uniqueID, long modifiedTime, ShapeAttributes attributes, Object globeStateKey) + { + super(uniqueID, modifiedTime); + + this.attributes = attributes; + this.globeStateKey = globeStateKey; + } + + @Override + @SuppressWarnings({"SimplifiableIfStatement"}) + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || this.getClass() != o.getClass()) + return false; + + SurfaceShapeStateKey that = (SurfaceShapeStateKey) o; + return super.equals(o) + && (this.attributes != null ? this.attributes.equals(that.attributes) : that.attributes == null) + && (this.globeStateKey != null ? this.globeStateKey.equals(that.globeStateKey) + : that.globeStateKey == null); + } + + @Override + public int hashCode() + { + int result = super.hashCode(); + result = 31 * result + (this.attributes != null ? this.attributes.hashCode() : 0); + result = 31 * result + (this.globeStateKey != null ? this.globeStateKey.hashCode() : 0); + return result; + } + + /** + * Returns the state key's size in bytes. Overridden to include the attributes and the reference to the globe + * state key. + * + * @return The state key's size in bytes. + */ + @Override + public long getSizeInBytes() + { + return super.getSizeInBytes() + 64; // Add the shape attributes and the references. + } + } + + //**************************************************************// + //******************** Cache Key, Cache Entry ****************// + //**************************************************************// + + protected static class GeometryKey + { + protected Globe globe; + protected double edgeIntervalsPerDegree; + + public GeometryKey(DrawContext dc, double edgeIntervalsPerDegree) + { + this.globe = dc.getGlobe(); + this.edgeIntervalsPerDegree = edgeIntervalsPerDegree; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || this.getClass() != o.getClass()) + return false; + + GeometryKey that = (GeometryKey) o; + return this.globe.equals(that.globe) && this.edgeIntervalsPerDegree == that.edgeIntervalsPerDegree; + } + + @Override + public int hashCode() + { + int hash = this.globe.hashCode(); + long temp = this.edgeIntervalsPerDegree != +0.0d ? Double.doubleToLongBits(this.edgeIntervalsPerDegree) + : 0L; + return 31 * hash + (int) (temp ^ (temp >>> 32)); + } + } + +// /** +// * Does this object support a certain export format? +// * +// * @param format Mime type for the export format. +// * +// * @return One of {@link Exportable#FORMAT_SUPPORTED}, {@link Exportable#FORMAT_NOT_SUPPORTED}, or {@link +// * Exportable#FORMAT_PARTIALLY_SUPPORTED}. +// * +// * @see #export(String, Object) +// */ +// public String isExportFormatSupported(String format) +// { +// if (KMLConstants.KML_MIME_TYPE.equalsIgnoreCase(format)) +// return Exportable.FORMAT_SUPPORTED; +// else +// return Exportable.FORMAT_NOT_SUPPORTED; +// } + +// /** +// * Export the Polygon. The {@code output} object will receive the exported data. The type of this object depends on +// * the export format. The formats and object types supported by this class are: +// *

    +// *

    +//     * Format                                         Supported output object types
    +//     * ================================================================================
    +//     * KML (application/vnd.google-earth.kml+xml)     java.io.Writer
    +//     *                                                java.io.OutputStream
    +//     *                                                javax.xml.stream.XMLStreamWriter
    +//     * 
    +// * +// * @param mimeType MIME type of desired export format. +// * @param output An object that will receive the exported data. The type of this object depends on the export +// * format (see above). +// * +// * @throws java.io.IOException If an exception occurs writing to the output object. +// * @throws UnsupportedOperationException if the format is not supported by this object, or if the {@code output} +// * argument is not of a supported type. +// */ +// public void export(String mimeType, Object output) throws IOException, UnsupportedOperationException +// { +// if (mimeType == null) +// { +// String message = Logging.getMessage("nullValue.Format"); +// Logging.error(message); +// throw new IllegalArgumentException(message); +// } +// +// if (output == null) +// { +// String message = Logging.getMessage("nullValue.OutputBufferIsNull"); +// Logging.error(message); +// throw new IllegalArgumentException(message); +// } +// +// if (KMLConstants.KML_MIME_TYPE.equalsIgnoreCase(mimeType)) +// { +// try +// { +// exportAsKML(output); +// } +// catch (XMLStreamException e) +// { +// Logging.logger().throwing(getClass().getName(), "export", e); +// throw new IOException(e); +// } +// } +// else +// { +// String message = Logging.getMessage("Export.UnsupportedFormat", mimeType); +// Logging.logger().warning(message); +// throw new UnsupportedOperationException(message); +// } +// } +// +// protected void exportAsKML(Object output) throws IOException, XMLStreamException +// { +// // This is a dummy method, here to enable a call to it above. It's expected to be overridden by subclasses. +// } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/render/BasicShapeAttributes.java b/WorldWindAndroid/src/gov/nasa/worldwind/render/BasicShapeAttributes.java index c4309d4..27c8a8a 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/render/BasicShapeAttributes.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/render/BasicShapeAttributes.java @@ -21,13 +21,21 @@ public class BasicShapeAttributes implements ShapeAttributes /** Indicates whether or not the shape's interior is drawn. Initially false. */ protected boolean enableInterior; /** Indicates the RGBA color of the shape's interior. Initially null. */ - protected Color interiorColor; + protected Material interiorMaterial; /** Indicates whether or not the shape's outline is drawn. Initially false. */ protected boolean enableOutline; /** Indicates the RGBA color of the shape's outline. Initially null. */ - protected Color outlineColor; + protected Material outlineMaterial; /** Indicates the line width (in pixels) used when rendering the shape's outline. Initially 0.0. */ protected double outlineWidth; + /** Indicates the image source that is applied as a texture to the shape's interior. Initially null. */ + protected Object imageSource; + /** Indicates the amount the balloon's texture is scaled by as a floating-point value. Initially 0.0. */ + protected double imageScale; + /** Indicates the opacity of the shape's interior as a floating-point value in the range 0.0 to 1.0. Initially 0.0. */ + protected double interiorOpacity; + /** Indicates the opacity of the shape's outline as a floating-point value in the range 0.0 to 1.0. Initially 0.0. */ + protected double outlineOpacity; /** * Creates a new BasicShapeAttributes with the default attributes. The default attributes are as follows: @@ -43,10 +51,12 @@ public BasicShapeAttributes() this.enableLighting = false; this.enableInterior = true; - this.interiorColor = Color.white(); // Returns a new instance; no need to insulate ourselves from changes. + this.interiorMaterial = Material.WHITE; // Returns a new instance; no need to insulate ourselves from changes. this.enableOutline = true; - this.outlineColor = Color.black(); // Returns a new instance; no need to insulate ourselves from changes. + this.outlineMaterial = Material.BLACK; // Returns a new instance; no need to insulate ourselves from changes. this.outlineWidth = 1; + this.imageSource = null; + this.imageScale = 1; } /** @@ -68,15 +78,15 @@ public BasicShapeAttributes(ShapeAttributes attributes) throw new IllegalArgumentException(msg); } - this.enableLighting = attributes.isEnableLighting(); - this.enableInterior = attributes.isEnableInterior(); - this.interiorColor = new Color(attributes.getInteriorColor()); // Copy to insulate ourselves from changes. - this.enableOutline = attributes.isEnableOutline(); - this.outlineColor = new Color(attributes.getOutlineColor()); // Copy to insulate ourselves from changes. - this.outlineWidth = attributes.getOutlineWidth(); + set(attributes); } - /** {@inheritDoc} */ + @Override + public ShapeAttributes copy() { + return new BasicShapeAttributes(this); + } + + @Override public void set(ShapeAttributes attributes) { if (attributes == null) @@ -88,55 +98,59 @@ public void set(ShapeAttributes attributes) this.enableLighting = attributes.isEnableLighting(); this.enableInterior = attributes.isEnableInterior(); - this.interiorColor.set(attributes.getInteriorColor()); // Copy to insulate ourselves from changes. + this.interiorMaterial = attributes.getInteriorMaterial(); // Copy to insulate ourselves from changes. this.enableOutline = attributes.isEnableOutline(); - this.outlineColor.set(attributes.getOutlineColor()); // Copy to insulate ourselves from changes. + this.outlineMaterial = attributes.getOutlineMaterial(); // Copy to insulate ourselves from changes. this.outlineWidth = attributes.getOutlineWidth(); + this.interiorOpacity = attributes.getInteriorOpacity(); + this.outlineOpacity = attributes.getOutlineOpacity(); + this.imageSource = attributes.getImageSource(); + this.imageScale = attributes.getImageScale(); } - /** {@inheritDoc} */ + @Override public boolean isUnresolved() { return unresolved; } - /** {@inheritDoc} */ + @Override public void setUnresolved(boolean unresolved) { this.unresolved = unresolved; } - /** {@inheritDoc} */ + @Override public boolean isEnableLighting() { return enableLighting; } - /** {@inheritDoc} */ + @Override public void setEnableLighting(boolean tf) { this.enableLighting = tf; } - /** {@inheritDoc} */ + @Override public boolean isEnableInterior() { return this.enableInterior; } - /** {@inheritDoc} */ + @Override public void setEnableInterior(boolean tf) { this.enableInterior = tf; } - /** {@inheritDoc} */ + @Override public Color getInteriorColor() { - return this.interiorColor; + return this.interiorMaterial.getDiffuse(); } - /** {@inheritDoc} */ + @Override public void setInteriorColor(Color color) { if (color == null) @@ -146,28 +160,28 @@ public void setInteriorColor(Color color) throw new IllegalArgumentException(msg); } - this.interiorColor.set(color); + this.interiorMaterial.getDiffuse().set(color); } - /** {@inheritDoc} */ + @Override public boolean isEnableOutline() { return this.enableOutline; } - /** {@inheritDoc} */ + @Override public void setEnableOutline(boolean tf) { this.enableOutline = tf; } - /** {@inheritDoc} */ + @Override public Color getOutlineColor() { - return this.outlineColor; + return this.outlineMaterial.getDiffuse(); } - /** {@inheritDoc} */ + @Override public void setOutlineColor(Color color) { if (color == null) @@ -177,16 +191,16 @@ public void setOutlineColor(Color color) throw new IllegalArgumentException(msg); } - this.outlineColor.set(color); + this.outlineMaterial.getDiffuse().set(color); } - /** {@inheritDoc} */ + @Override public double getOutlineWidth() { return this.outlineWidth; } - /** {@inheritDoc} */ + @Override public void setOutlineWidth(double width) { if (width <= 0) @@ -198,4 +212,64 @@ public void setOutlineWidth(double width) this.outlineWidth = width; } + + @Override + public Material getInteriorMaterial() { + return this.interiorMaterial; + } + + @Override + public void setInteriorMaterial(Material material) { + this.interiorMaterial = material; + } + + @Override + public Material getOutlineMaterial() { + return this.outlineMaterial; + } + + @Override + public void setOutlineMaterial(Material material) { + this.outlineMaterial = material; + } + + @Override + public double getInteriorOpacity() { + return this.interiorOpacity; + } + + @Override + public void setInteriorOpacity(double opacity) { + this.interiorOpacity = opacity; + } + + @Override + public double getOutlineOpacity() { + return this.outlineOpacity; + } + + @Override + public void setOutlineOpacity(double opacity) { + this.outlineOpacity = opacity; + } + + @Override + public Object getImageSource() { + return this.imageSource; + } + + @Override + public void setImageSource(Object imageSource) { + this.imageSource = imageSource; + } + + @Override + public double getImageScale() { + return this.imageScale; + } + + @Override + public void setImageScale(double scale) { + this.imageScale = scale; + } } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/render/DrawContext.java b/WorldWindAndroid/src/gov/nasa/worldwind/render/DrawContext.java index 791408c..52c94ec 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/render/DrawContext.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/render/DrawContext.java @@ -4,13 +4,12 @@ */ package gov.nasa.worldwind.render; -import gov.nasa.worldwind.Model; -import gov.nasa.worldwind.View; -import gov.nasa.worldwind.WWObjectImpl; +import android.graphics.Point; +import android.opengl.GLES20; +import gov.nasa.worldwind.*; +import gov.nasa.worldwind.avlist.AVKey; import gov.nasa.worldwind.cache.GpuResourceCache; -import gov.nasa.worldwind.geom.Extent; -import gov.nasa.worldwind.geom.Position; -import gov.nasa.worldwind.geom.Sector; +import gov.nasa.worldwind.geom.*; import gov.nasa.worldwind.globes.Globe; import gov.nasa.worldwind.layers.Layer; import gov.nasa.worldwind.layers.LayerList; @@ -19,20 +18,21 @@ import gov.nasa.worldwind.terrain.SectorGeometryList; import gov.nasa.worldwind.terrain.Terrain; import gov.nasa.worldwind.terrain.VisibleTerrain; -import gov.nasa.worldwind.util.BufferUtil; -import gov.nasa.worldwind.util.Logging; +import gov.nasa.worldwind.util.*; + import java.nio.ByteBuffer; -import java.util.PriorityQueue; -import android.graphics.Point; -import android.opengl.GLES20; +import java.util.*; + +import static android.opengl.GLES20.*; /** * Edited By: Nicola Dorigatti, Trilogis - * + * * @author dcollins * @version $Id: DrawContext.java 834 2012-10-08 22:25:55Z dcollins $ */ -public class DrawContext extends WWObjectImpl { +public class DrawContext extends WWObjectImpl implements Disposable { + protected static class OrderedRenderableEntry implements Comparable { protected OrderedRenderable or; protected double distanceFromEye; @@ -65,6 +65,7 @@ public int compareTo(OrderedRenderableEntry that) { protected double verticalExaggeration = DEFAULT_VERTICAL_EXAGGERATION; protected GpuResourceCache gpuResourceCache; protected long frameTimestamp; + protected double deltaTime; protected Sector visibleSector; protected Terrain visibleTerrain = new VisibleTerrain(this); protected SectorGeometryList surfaceGeometry; @@ -72,7 +73,13 @@ public int compareTo(OrderedRenderableEntry that) { protected Layer currentLayer; protected GpuProgram currentProgram; protected boolean orderedRenderingMode; + protected boolean preRenderMode = false; protected PriorityQueue orderedRenderables = new PriorityQueue(100); + + /** Use a standard Queue to store the ordered surface object renderables. Ordered surface renderables are processed + in the order they were submitted. */ + protected Queue orderedSurfaceRenderables = new ArrayDeque(); + protected boolean pickingMode; protected boolean deepPickingMode; protected int uniquePickNumber; @@ -80,6 +87,12 @@ public int compareTo(OrderedRenderableEntry that) { protected Point pickPoint; protected PickedObjectList objectsAtPickPoint = new PickedObjectList(); + protected PickPointFrustumList pickFrustumList = new PickPointFrustumList(); + protected Point pickPointFrustumDimension = new Point(3, 3); + + protected Set perFrameStatisticsKeys; + protected Map perFrameStatistics; + /** * Initializes this DrawContext. This method should be called at the beginning of each frame to prepare * the DrawContext for the coming render pass. @@ -99,10 +112,6 @@ public void initialize(int viewportWidth, int viewportHeight) { this.viewportWidth = viewportWidth; this.viewportHeight = viewportHeight; - this.model = null; - this.view = null; - this.verticalExaggeration = DEFAULT_VERTICAL_EXAGGERATION; - this.gpuResourceCache = null; this.frameTimestamp = 0; this.visibleSector = null; this.surfaceGeometry = null; @@ -110,6 +119,7 @@ public void initialize(int viewportWidth, int viewportHeight) { this.currentProgram = null; this.orderedRenderingMode = false; this.orderedRenderables.clear(); + this.orderedSurfaceRenderables.clear(); this.pickingMode = false; this.deepPickingMode = false; this.uniquePickNumber = 0; @@ -117,6 +127,15 @@ public void initialize(int viewportWidth, int viewportHeight) { this.objectsAtPickPoint.clear(); } + /** + * Free internal resources held by this draw context. A GL context must be current when this method is called. + */ + @Override + public void dispose() + { + + } + public int getViewportWidth() { return this.viewportWidth; } @@ -130,7 +149,7 @@ public int getViewportHeight() { * information on the color int format. *

    * The returned int can be converted to a floating-point RGBA color using the Color constructor {@link Color#Color(int)}. - * + * * @return a packed 32-bit ARGB color representing the background color. */ public int getClearColor() { @@ -139,7 +158,7 @@ public int getClearColor() { /** * Retrieves the current Model, which may be null. - * + * * @return the current Model, which may be null */ public Model getModel() { @@ -149,7 +168,7 @@ public Model getModel() { /** * Assign a new Model. Some layers cannot function properly with a null Model. It is * recommended that the Model is never set to null during a normal render pass. - * + * * @param model * the new Model */ @@ -159,7 +178,7 @@ public void setModel(Model model) { /** * Retrieves the current View, which may be null. - * + * * @return the current View, which may be null */ public View getView() { @@ -169,7 +188,7 @@ public View getView() { /** * Assigns a new View. Some layers cannot function properly with a null View. It is * recommended that the View is never set to null during a normal render pass. - * + * * @param view * the enw View */ @@ -179,7 +198,7 @@ public void setView(View view) { /** * Retrieves the current Globe, which may be null. - * + * * @return the current Globe, which may be null */ public Globe getGlobe() { @@ -188,7 +207,7 @@ public Globe getGlobe() { /** * Retrieves a list containing all the current layers. No guarantee is made about the order of the layers. - * + * * @return a LayerList containing all the current layers */ public LayerList getLayers() { @@ -200,7 +219,7 @@ public LayerList getLayers() { * elevation. A vertical exaggeration of zero creates a surface which exactly fits the shape of the underlying Globe. A vertical exaggeration * of 3 will create mountains and valleys which are three times as * high/deep as they really are. - * + * * @return the current vertical exaggeration */ public double getVerticalExaggeration() { @@ -212,26 +231,43 @@ public double getVerticalExaggeration() { * vertical exaggeration of zero creates a surface which exactly fits the shape of the underlying Globe. A vertical exaggeration of 3 will * create mountains and valleys which are three times as * high/deep as they really are. - * + * * @param verticalExaggeration * the new vertical exaggeration. */ public void setVerticalExaggeration(double verticalExaggeration) { - this.verticalExaggeration = verticalExaggeration; + if(this.verticalExaggeration != verticalExaggeration) { + double oldVE = this.verticalExaggeration; + this.verticalExaggeration = verticalExaggeration; + firePropertyChange(AVKey.VERTICAL_EXAGGERATION, oldVE, this.verticalExaggeration); + } } /** * Returns the GPU resource cache used by this draw context. - * + * * @return the GPU resource cache used by this draw context. */ public GpuResourceCache getGpuResourceCache() { return this.gpuResourceCache; } + /** + * Returns the GPU resource cache for this draw context. This method returns the same value as {@link + * #getGpuResourceCache()}. + * + * @return the GPU resource cache for this draw context. + * + * @see #getGpuResourceCache() + */ + public GpuResourceCache getTextureCache() + { + return this.gpuResourceCache; + } + /** * Specifies the GPU resource cache for this draw context. - * + * * @param gpuResourceCache * the GPU resource cache for this draw context. */ @@ -245,11 +281,27 @@ public void setGpuResourceCache(GpuResourceCache gpuResourceCache) { this.gpuResourceCache = gpuResourceCache; } + /** + * Time time since previous frame + * @return ms since last rendered frame + */ + public double getDeltaTime() { + return this.deltaTime; + } + + /** + * Time time since previous frame + * @param deltaTime time elapsed since last frame (ms) + */ + public void setDeltaTime(double deltaTime) { + this.deltaTime = deltaTime; + } + /** * Returns the time stamp corresponding to the beginning of a pre-render, pick, render sequence. The stamp remains * constant across these three operations so that called objects may avoid recomputing the same values during each * of the calls in the sequence. - * + * * @return the frame time stamp. See {@link System#currentTimeMillis()} for its numerical meaning. */ public long getFrameTimeStamp() { @@ -260,7 +312,7 @@ public long getFrameTimeStamp() { * Specifies the time stamp corresponding to the beginning of a pre-render, pick, render sequence. The stamp must * remain constant across these three operations so that called objects may avoid recomputing the same values during * each of the calls in the sequence. - * + * * @param timeStamp * the frame time stamp. See {@link System#currentTimeMillis()} for its numerical meaning. */ @@ -271,7 +323,7 @@ public void setFrameTimeStamp(long timeStamp) { /** * Retrieves a Sector which is at least as large as the current visible sector. The value returned is * the value passed to SetVisibleSector. This method may return null. - * + * * @return a Sector at least the size of the current visible sector, null if unavailable */ public Sector getVisibleSector() { @@ -281,7 +333,7 @@ public Sector getVisibleSector() { /** * Sets the visible Sector. The new visible sector must completely encompass the Sector which is * visible on the display. - * + * * @param sector * the new visible Sector */ @@ -296,16 +348,47 @@ public void setVisibleSector(Sector sector) { *

    * The returned interface does not provide any method for drawing the currently visible terrain geometry. The methods {@link #getSurfaceGeometry()} and * {@link #getSurfaceTileRenderer()} return interfaces suited for terrain rendering. - * + * * @return an interface to perform queries against the currently visible terrain. */ public Terrain getVisibleTerrain() { return this.visibleTerrain; } + public Vec4 getPointOnTerrain(Angle latitude, Angle longitude) + { + if (latitude == null || longitude == null) + { + String message = Logging.getMessage("nullValue.LatitudeOrLongitudeIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (this.getVisibleSector() == null) + return null; + + if (!this.getVisibleSector().contains(latitude, longitude)) + return null; + + SectorGeometryList sectorGeometry = this.getSurfaceGeometry(); + if (sectorGeometry != null) + { + Vec4 p = new Vec4(); + sectorGeometry.getSurfacePoint(latitude, longitude, p); + if (p != null) + return p; + } + + return null; + } + + public Vec4 getPointOnTerrain(LatLon point) { + return getPointOnTerrain(point.latitude, point.longitude); + } + /** * Indicates the surface geometry that is visible this frame. - * + * * @return the visible surface geometry. */ public SectorGeometryList getSurfaceGeometry() { @@ -314,7 +397,7 @@ public SectorGeometryList getSurfaceGeometry() { /** * Specifies the surface geometry that is visible this frame. - * + * * @param surfaceGeometry * the visible surface geometry. */ @@ -329,7 +412,7 @@ public SurfaceTileRenderer getSurfaceTileRenderer() { /** * Returns the current layer. The field is informative only and enables layer contents to determine their containing * layer. - * + * * @return the current layer, or null if no layer is current. */ public Layer getCurrentLayer() { @@ -339,7 +422,7 @@ public Layer getCurrentLayer() { /** * Sets the current layer field to the specified layer or null. The field is informative only and enables layer * contents to determine their containing layer. - * + * * @param layer * the current layer or null. */ @@ -355,6 +438,45 @@ public void setCurrentProgram(GpuProgram program) { this.currentProgram = program; } + public void restoreDefaultBlending() + { + glBlendFunc(GL_ONE, GL_ZERO); + glDisable(GL_BLEND); + } + + public void restoreDefaultCurrentColor() + { + getCurrentProgram().loadUniformColor("uColor", Color.white()); + } + + public void restoreDefaultDepthTesting() + { + glEnable(GL_DEPTH_TEST); + glDepthMask(true); + } + + /** + * Indicates whether the scene controller is currently pre-rendering. + * + * @return true if pre-rendering, otherwise false. + * + * @see PreRenderable + */ + public boolean isPreRenderMode() { + return preRenderMode; + } + + /** + * Specifies whether the scene controller is pre-rendering. + * + * @param preRenderMode true to indicate pre-rendering, otherwise false. + * + * @see PreRenderable + */ + public void setPreRenderMode(boolean preRenderMode) { + this.preRenderMode = preRenderMode; + } + public boolean isOrderedRenderingMode() { return this.orderedRenderingMode; } @@ -398,12 +520,126 @@ public void addOrderedRenderableToBack(OrderedRenderable orderedRenderable) { this.orderedRenderables.add(new OrderedRenderableEntry(orderedRenderable, Double.MAX_VALUE, System.nanoTime())); } + /** + * Adds an {@link gov.nasa.worldwind.render.OrderedRenderable} to the draw context's ordered surface renderable + * queue. This queue is populated during layer rendering with objects to render on the terrain surface, and is + * processed immediately after layer rendering. + * + * @param orderedRenderable the ordered renderable to add. + */ + public void addOrderedSurfaceRenderable(OrderedRenderable orderedRenderable) + { + if (orderedRenderable == null) + { + String msg = Logging.getMessage("nullValue.OrderedRenderable"); + Logging.warning(msg); + return; // benign event + } + + this.orderedSurfaceRenderables.add(orderedRenderable); + } + + /** + * Returns the draw context's ordered surface renderable queue. This queue is populated during layer rendering with + * objects to render on the terrain surface, and is processed immediately after layer rendering. + * + * @return the draw context's ordered surface renderable queue. + */ + public Queue getOrderedSurfaceRenderables() + { + return this.orderedSurfaceRenderables; + } + + public PickPointFrustumList getPickFrustums() + { + return this.pickFrustumList; + } + + public void setPickPointFrustumDimension(Point dim) + { + if (dim == null) + { + String message = Logging.getMessage("nullValue.DimensionIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (dim.x < 3 || dim.y < 3) + { + String message = Logging.getMessage("DrawContext.PickPointFrustumDimensionTooSmall"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.pickPointFrustumDimension = new Point(dim); + } + + public android.graphics.Point getPickPointFrustumDimension() + { + return this.pickPointFrustumDimension; + } + + public void addPickPointFrustum() + { + //Compute the current picking frustum + if (getPickPoint() != null) + { + Rect viewport = getView().getViewport(); + + double viewportWidth = viewport.width <= 0.0 ? 1.0 : viewport.width; + double viewportHeight = viewport.height <= 0.0 ? 1.0 : viewport.height; + + //Get the pick point and translate screen center to zero + Point ptCenter = new Point(getPickPoint()); + ptCenter.y = (int) viewportHeight - ptCenter.y; + ptCenter.x += (int) (-viewportWidth / 2); + ptCenter.y += (int) (-viewportHeight / 2); + + //Number of pixels around pick point to include in frustum + int offsetX = pickPointFrustumDimension.x / 2; + int offsetY = pickPointFrustumDimension.y / 2; + + //If the frustum is not valid then don't add it and return silently + if (offsetX == 0 || offsetY == 0) + return; + + //Compute the distance to the near plane in screen coordinates + double width = getView().getFieldOfView().tanHalfAngle() * getView().getNearClipDistance(); + double x = width / (viewportWidth / 2.0); + double screenDist = getView().getNearClipDistance() / x; + + //Create the four vectors that define the top-left, top-right, bottom-left, and bottom-right vectors + Vec4 vTL = new Vec4(ptCenter.x - offsetX, ptCenter.y + offsetY, -screenDist); + Vec4 vTR = new Vec4(ptCenter.x + offsetX, ptCenter.y + offsetY, -screenDist); + Vec4 vBL = new Vec4(ptCenter.x - offsetX, ptCenter.y - offsetY, -screenDist); + Vec4 vBR = new Vec4(ptCenter.x + offsetX, ptCenter.y - offsetY, -screenDist); + + //Compute the frustum from these four vectors + Frustum frustum = Frustum.fromPerspectiveVecs(vTL, vTR, vBL, vBR, + getView().getNearClipDistance(), getView().getFarClipDistance()); + + //Create the screen rectangle associated with this frustum + android.graphics.Rect rectScreen = new android.graphics.Rect(getPickPoint().x - offsetX, + (int) viewportHeight - getPickPoint().y - offsetY, + pickPointFrustumDimension.x, + pickPointFrustumDimension.y); + + //Transform the frustum to Model Coordinates + Matrix modelviewTranspose = Matrix.fromIdentity(); + modelviewTranspose.transpose(getView().getModelviewMatrix()); + if (modelviewTranspose != null) + frustum = frustum.transformBy(modelviewTranspose); + + this.pickFrustumList.add(new PickPointFrustum(frustum, rectScreen)); + } + } + /** * Indicates whether the drawing is occurring in picking picking mode. In picking mode, each unique object is drawn * in a unique RGB color by calling {@link #getUniquePickColor()} prior to rendering. Any OpenGL state that could * cause an object to draw a color other than the unique RGB pick color must be disabled. This includes * antialiasing, blending, and dithering. - * + * * @return true if drawing should occur in picking mode, otherwise false. */ public boolean isPickingMode() { @@ -412,7 +648,7 @@ public boolean isPickingMode() { /** * Specifies whether drawing should occur in picking mode. See {@link #isPickingMode()} for more information. - * + * * @param tf * true to specify that drawing should occur in picking mode, otherwise false. */ @@ -422,7 +658,7 @@ public void setPickingMode(boolean tf) { /** * Indicates whether all items under the pick point are picked. - * + * * @return true if all items under the pick point are picked, otherwise false . */ public boolean isDeepPickingEnabled() { @@ -431,7 +667,7 @@ public boolean isDeepPickingEnabled() { /** * Specifies whether all items under the pick point are picked. - * + * * @param tf * true to pick all objects under the pick point. */ @@ -446,20 +682,20 @@ public void setDeepPickingEnabled(boolean tf) { *

    * The returned int can be converted to a floating-point RGB color using the Color constructor {@link Color#Color(int, boolean)} and passing * false in hasAlpha. - * + * * @return a packed 32-bit RGB color representing a unique pick color. */ public int getUniquePickColor() { this.uniquePickNumber++; // Increment to the next pick number. This causes the pick numbers to start at 1. if (this.uniquePickNumber == this.clearColor) // Skip the clear color. - this.uniquePickNumber++; + this.uniquePickNumber++; if (this.uniquePickNumber >= 0x00FFFFFF) // We have run out of available pick numbers. { this.uniquePickNumber = 1; // Do not use black or white as a pick color. Pick numbers start at 1. if (this.uniquePickNumber == this.clearColor) // Skip the clear color. - this.uniquePickNumber++; + this.uniquePickNumber++; } return this.uniquePickNumber; @@ -474,7 +710,7 @@ public int getUniquePickColor() { *

    * The returned int can be converted to a floating-point RGB color using the Color constructor {@link Color#Color(int, boolean)} and passing * false in hasAlpha. - * + * * @param point * the screen point who's RGB color is returned. * @return a packed 32-bit RGB color representing the color at the screen point, or 0 if the point indicates a pixel @@ -494,6 +730,7 @@ public int getPickColor(Point point) { // y coordinate from system screen coordinates to OpenGL screen coordinates. int yInGLCoords = this.viewportHeight - point.y; GLES20.glReadPixels(point.x, yInGLCoords, 1, 1, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, this.pickColor); + WorldWindowImpl.glCheckError("glReadPixels"); // OpenGL places the pixel's RGBA components in the first 4 bytes of the buffer, in that order. We ignore the // alpha component and compose a packed 24-bit RGB color int equivalent to those returned by getUniquePickColor. @@ -503,7 +740,7 @@ public int getPickColor(Point point) { /** * Returns the current pick point. - * + * * @return the current pick point, or null if no pick point is available. */ public Point getPickPoint() { @@ -512,7 +749,7 @@ public Point getPickPoint() { /** * Specifies the pick point. - * + * * @param point * the pick point, or null to indicate there is no pick point. */ @@ -522,7 +759,7 @@ public void setPickPoint(Point point) { /** * Indicates the geographic coordinates of the point on the terrain at the current viewport's center. - * + * * @return the geographic coordinates of the current viewport's center. Returns null if the globe's surface is not * under the viewport's center point. */ @@ -537,7 +774,7 @@ public void setViewportCenterPosition(Position viewportCenterPosition) { /** * Returns the World Wind objects at the current pick point. The list of objects is determined while drawing in * picking mode, and is cleared each time this draw context is initialized. - * + * * @return the list of currently picked objects. */ public PickedObjectList getObjectsAtPickPoint() { @@ -546,7 +783,7 @@ public PickedObjectList getObjectsAtPickPoint() { /** * Adds a single picked object to the current picked-object list. - * + * * @param pickedObject * the object to add. * @throws IllegalArgumentException @@ -564,7 +801,7 @@ public void addPickedObject(PickedObject pickedObject) { /** * Indicates whether a specified extent is smaller than a specified number of pixels for the current view. - * + * * @param extent * the extent to test. May be null, in which case this method returns false. * @param numPixels @@ -594,7 +831,7 @@ public boolean isSmall(Extent extent, int numPixels) { * Performs a multi-pass rendering technique to ensure that outlines around filled shapes are drawn correctly when * blending or ant-aliasing is performed, and that filled portions of the shape resolve depth-buffer fighting with * shapes previously drawn in favor of the current shape. - * + * * @param renderer * an object implementing the {@link gov.nasa.worldwind.render.OutlinedShape} interface for the * shape. @@ -632,7 +869,7 @@ public void drawOutlinedShape(OutlinedShape renderer, Object shape) { if (renderer.isDrawInterior(this, shape)) renderer.drawInterior(this, shape); if (renderer.isDrawOutline(this, shape)) // the line might extend outside the interior's projection - renderer.drawOutline(this, shape); + renderer.drawOutline(this, shape); return; } @@ -643,7 +880,9 @@ public void drawOutlinedShape(OutlinedShape renderer, Object shape) { // the outline is be visible behind the potentially transparent interior. if (renderer.isDrawOutline(this, shape) && renderer.isDrawInterior(this, shape)) { GLES20.glColorMask(true, true, true, true); + WorldWindowImpl.glCheckError("glColorMask"); GLES20.glDepthMask(false); + WorldWindowImpl.glCheckError("glDepthMask"); renderer.drawOutline(this, shape); } @@ -661,22 +900,31 @@ public void drawOutlinedShape(OutlinedShape renderer, Object shape) { Double depthOffsetFactor = renderer.getDepthOffsetFactor(this, shape); Double depthOffsetUnits = renderer.getDepthOffsetUnits(this, shape); GLES20.glColorMask(false, false, false, false); + WorldWindowImpl.glCheckError("glColorMask"); GLES20.glDepthMask(true); + WorldWindowImpl.glCheckError("glDepthMask"); GLES20.glEnable(GLES20.GL_POLYGON_OFFSET_FILL); + WorldWindowImpl.glCheckError("glEnable: GL_POLYGON_OFFSET_FILL"); GLES20.glPolygonOffset(depthOffsetFactor != null ? depthOffsetFactor.floatValue() : DEFAULT_DEPTH_OFFSET_FACTOR, depthOffsetUnits != null ? depthOffsetUnits.floatValue() : DEFAULT_DEPTH_OFFSET_UNITS); + WorldWindowImpl.glCheckError("glPolygonOffset"); renderer.drawInterior(this, shape); // Draw color. GLES20.glColorMask(true, true, true, true); + WorldWindowImpl.glCheckError("glColorMask"); GLES20.glDepthMask(false); + WorldWindowImpl.glCheckError("glDepthMask"); GLES20.glDisable(GLES20.GL_POLYGON_OFFSET_FILL); + WorldWindowImpl.glCheckError("glDisable: GL_POLYGON_OFFSET_FILL"); renderer.drawInterior(this, shape); } else { GLES20.glColorMask(true, true, true, true); + WorldWindowImpl.glCheckError("glColorMask"); GLES20.glDepthMask(true); + WorldWindowImpl.glCheckError("glDepthMask"); renderer.drawInterior(this, shape); } @@ -686,16 +934,81 @@ public void drawOutlinedShape(OutlinedShape renderer, Object shape) { // the interior colors. if (renderer.isDrawOutline(this, shape)) { GLES20.glColorMask(true, true, true, true); + WorldWindowImpl.glCheckError("glColorMask"); GLES20.glDepthMask(true); + WorldWindowImpl.glCheckError("glDepthMask"); renderer.drawOutline(this, shape); } } finally { // Restore the default GL state values we modified above. GLES20.glDisable(GLES20.GL_POLYGON_OFFSET_FILL); + WorldWindowImpl.glCheckError("glDisable"); GLES20.glColorMask(true, true, true, true); + WorldWindowImpl.glCheckError("glColorMask"); GLES20.glDepthMask(true); + WorldWindowImpl.glCheckError("glDepthMask"); GLES20.glPolygonOffset(0f, 0f); + WorldWindowImpl.glCheckError("glPolygonOffset"); + } + } + + + public Map getPerFrameStatistics() + { + return this.perFrameStatistics; + } + + + public void setPerFrameStatisticsKeys(Set statKeys, Map stats) + { + this.perFrameStatisticsKeys = statKeys; + this.perFrameStatistics = stats; + } + + public Set getPerFrameStatisticsKeys() + { + return perFrameStatisticsKeys; + } + + public void setPerFrameStatistic(String key, String displayName, Object value) + { + if (this.perFrameStatistics == null || this.perFrameStatisticsKeys == null) + return; + + if (key == null) + { + String message = Logging.getMessage("nullValue.KeyIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (displayName == null) + { + String message = Logging.getMessage("nullValue.DisplayNameIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (this.perFrameStatisticsKeys.contains(key) || this.perFrameStatisticsKeys.contains(PerformanceStatistic.ALL)) + this.perFrameStatistics.put(key, new PerformanceStatistic(key, displayName, value)); + } + + public void setPerFrameStatistics(Collection stats) + { + if (stats == null) + { + String message = Logging.getMessage("nullValue.ListIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (this.perFrameStatistics == null || this.perFrameStatisticsKeys == null) + return; + + for (PerformanceStatistic stat : stats) + { + this.perFrameStatistics.put(stat.getKey(), stat); } } } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/render/GpuProgram.java b/WorldWindAndroid/src/gov/nasa/worldwind/render/GpuProgram.java index b478734..8b36888 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/render/GpuProgram.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/render/GpuProgram.java @@ -4,7 +4,9 @@ */ package gov.nasa.worldwind.render; +import android.opengl.GLES20; import gov.nasa.worldwind.Disposable; +import gov.nasa.worldwind.WorldWindowImpl; import gov.nasa.worldwind.cache.Cacheable; import gov.nasa.worldwind.exception.WWRuntimeException; import gov.nasa.worldwind.geom.Matrix; @@ -12,15 +14,19 @@ import gov.nasa.worldwind.util.Logging; import gov.nasa.worldwind.util.WWIO; import gov.nasa.worldwind.util.WWUtil; + import java.io.IOException; import java.io.InputStream; +import java.nio.Buffer; import java.util.HashMap; import java.util.Map; -import android.opengl.GLES20; + +import static gov.nasa.worldwind.WorldWindowImpl.glCheckError; +import static android.opengl.GLES20.*; /** * Edited By: Nicola Dorigatti, Trilogis - * + * * @author dcollins * @version $Id: GpuProgram.java 806 2012-09-26 01:50:13Z dcollins $ */ @@ -133,14 +139,20 @@ protected void initWithSource(String vertexSource, String fragmentSource) { } GLES20.glAttachShader(program, vertexShader.getShaderId()); + glCheckError("glAttachShader"); GLES20.glAttachShader(program, fragmentShader.getShaderId()); + glCheckError("glAttachShader"); if (!this.link(program)) { // Get the info log before deleting the program object. String infoLog = GLES20.glGetProgramInfoLog(program); + glCheckError("glGetProgramInfoLog"); GLES20.glDetachShader(program, vertexShader.getShaderId()); + glCheckError("glDetachShader"); GLES20.glDetachShader(program, fragmentShader.getShaderId()); + glCheckError("glDetachShader"); GLES20.glDeleteProgram(program); + glCheckError("glDeleteProgram"); vertexShader.dispose(); fragmentShader.dispose(); @@ -161,6 +173,9 @@ protected void init() { } public static GpuProgramSource readProgramSource(Object vertexSource, Object fragmentSource) throws IOException { + if(WorldWindowImpl.DEBUG) + Logging.info(String.format("Loading Program source: %s, %s", vertexSource, fragmentSource)); + if (WWUtil.isEmpty(vertexSource)) { String msg = Logging.getMessage("nullValue.VertexSourceIsNull"); throw new IllegalArgumentException(msg); @@ -222,14 +237,18 @@ public long getSizeInBytes() { public void bind() { GLES20.glUseProgram(this.programId); + glCheckError("glUseProgram"); } public void dispose() { if (this.programId != 0) { if (this.vertexShader != null) GLES20.glDetachShader(this.programId, this.vertexShader.getShaderId()); + glCheckError("glDetachShader"); if (this.fragmentShader != null) GLES20.glDetachShader(this.programId, this.fragmentShader.getShaderId()); + glCheckError("glDetachShader"); GLES20.glDeleteProgram(this.programId); + glCheckError("glDeleteProgram"); this.programId = 0; } @@ -257,12 +276,48 @@ public int getAttribLocation(String name) { // starts with "gl_". In either case, we store the value -1 in our map since the return value does not // change until the program is linked again. location = GLES20.glGetAttribLocation(this.programId, name); + glCheckError("glGetAttribLocation"); this.attribLocations.put(name, location); } return location; } + public void enableVertexAttribute(String name) { + int attribLocation = getAttribLocation(name); + if (attribLocation >= 0) { + GLES20.glEnableVertexAttribArray(attribLocation); + glCheckError("glEnableVertexAttribArray"); + } + } + + public void disableVertexAttribute(String name) { + int attribLocation = getAttribLocation(name); + if (attribLocation >= 0) { + GLES20.glDisableVertexAttribArray(attribLocation); + glCheckError("glDisableVertexAttribArray"); + } + } + + public void vertexAttribPointer(String name, int size, int type, boolean normalized, int stride, int offset) { + int colorLocation = getAttribLocation(name); + if (colorLocation < 0) { + Logging.warning(Logging.getMessage("GL.VertexAttributeIsMissing", "vertexColor")); + } + GLES20.glVertexAttribPointer(colorLocation, size, type, normalized, stride, offset); + glCheckError("glVertexAttribPointer"); + } + + public void vertexAttribPointer(String name, int size, int type, boolean normalized, int stride, Buffer buffer) { + int colorLocation = getAttribLocation(name); + if (colorLocation < 0) { + Logging.warning(Logging.getMessage("GL.VertexAttributeIsMissing", "vertexColor")); + } + GLES20.glVertexAttribPointer(colorLocation, size, type, normalized, stride, buffer); + glCheckError("glVertexAttribPointer"); + } + + public int getUniformLocation(String name) { if (WWUtil.isEmpty(name)) { String msg = Logging.getMessage("nullValue.NameIsNull"); @@ -276,12 +331,31 @@ public int getUniformLocation(String name) { // name starts with "gl_". In either case, we store the value -1 in our map since the return value does not // change until the program is linked again. location = GLES20.glGetUniformLocation(this.programId, name); + glCheckError("glGetUniformLocation"); this.uniformLocations.put(name, location); } return location; } + public void loadUniform1b(String name, boolean b) { + if (WWUtil.isEmpty(name)) { + String msg = Logging.getMessage("nullValue.NameIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + int location = this.getUniformLocation(name); + if (location < 0) { + String msg = Logging.getMessage("GL.UniformNameIsInvalid", name); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + GLES20.glUniform1i(location, b ? 1 : 0); + glCheckError("glUniform1f"); + } + public void loadUniform1f(String name, double x) { if (WWUtil.isEmpty(name)) { String msg = Logging.getMessage("nullValue.NameIsNull"); @@ -297,6 +371,7 @@ public void loadUniform1f(String name, double x) { } GLES20.glUniform1f(location, (float) x); + glCheckError("glUniform1f"); } public void loadUniform2f(String name, double x, double y) { @@ -314,6 +389,7 @@ public void loadUniform2f(String name, double x, double y) { } GLES20.glUniform2f(location, (float) x, (float) y); + glCheckError("glUniform2f"); } public void loadUniform3f(String name, double x, double y, double z) { @@ -331,6 +407,7 @@ public void loadUniform3f(String name, double x, double y, double z) { } GLES20.glUniform3f(location, (float) x, (float) y, (float) z); + glCheckError("glUniform3f"); } public void loadUniform4f(String name, double x, double y, double z, double w) { @@ -348,29 +425,11 @@ public void loadUniform4f(String name, double x, double y, double z, double w) { } GLES20.glUniform4f(location, (float) x, (float) y, (float) z, (float) w); + glCheckError("glUniform4f"); } public void loadUniformVec4(String name, Vec4 vec) { - if (WWUtil.isEmpty(name)) { - String msg = Logging.getMessage("nullValue.NameIsNull"); - Logging.error(msg); - throw new IllegalArgumentException(msg); - } - - int location = this.getUniformLocation(name); - if (location < 0) { - String msg = Logging.getMessage("GL.UniformNameIsInvalid", name); - Logging.error(msg); - throw new IllegalArgumentException(msg); - } - - if (vec == null) { - String msg = Logging.getMessage("nullValue.VectorIsNull"); - Logging.error(msg); - throw new IllegalArgumentException(msg); - } - - GLES20.glUniform4f(location, (float) vec.x, (float) vec.y, (float) vec.z, (float) vec.w); + loadUniform4f(name, vec.x, vec.y, vec.z, vec.w); } public void loadUniformMatrix(String name, Matrix matrix) { @@ -418,6 +477,7 @@ public void loadUniformMatrix(String name, Matrix matrix) { m[15] = (float) matrix.m[15]; GLES20.glUniformMatrix4fv(location, 1, false, m, 0); + glCheckError("glUniformMatrix4fv"); } public void loadUniformColor(String name, Color color) { @@ -435,6 +495,7 @@ public void loadUniformColor(String name, Color color) { } GLES20.glUniform4f(location, (float) color.r, (float) color.g, (float) color.b, (float) color.a); + glCheckError("glUniform4f"); } public void loadUniformSampler(String name, int value) { @@ -452,13 +513,16 @@ public void loadUniformSampler(String name, int value) { } GLES20.glUniform1i(location, value); + glCheckError("glUniform1i"); } protected boolean link(int program) { int[] linkStatus = new int[1]; GLES20.glLinkProgram(program); + glCheckError("glLinkProgram"); GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); + glCheckError("glGetProgramiv"); return linkStatus[0] == GLES20.GL_TRUE; } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/render/GpuShader.java b/WorldWindAndroid/src/gov/nasa/worldwind/render/GpuShader.java index 448f49c..06e9c08 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/render/GpuShader.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/render/GpuShader.java @@ -7,9 +7,11 @@ import android.opengl.GLES20; import gov.nasa.worldwind.Disposable; +import gov.nasa.worldwind.WorldWindowImpl; import gov.nasa.worldwind.cache.Cacheable; import gov.nasa.worldwind.exception.WWRuntimeException; -import gov.nasa.worldwind.util.*; +import gov.nasa.worldwind.util.Logging; +import gov.nasa.worldwind.util.WWUtil; /** * @author dcollins @@ -17,129 +19,136 @@ */ public class GpuShader implements Cacheable, Disposable { - protected int type; - protected int shaderId; - protected long estimatedMemorySize; - - public GpuShader(int type, String source) - { - if (type != GLES20.GL_VERTEX_SHADER && type != GLES20.GL_FRAGMENT_SHADER) - { - String msg = Logging.getMessage("generic.TypeIsInvalid", type); - Logging.error(msg); - throw new IllegalArgumentException(msg); - } - - if (WWUtil.isEmpty(source)) - { - String msg = Logging.getMessage("nullValue.SourceIsNull"); - Logging.error(msg); - throw new IllegalArgumentException(msg); - } - - int shader = GLES20.glCreateShader(type); - if (shader <= 0) - { - String msg = Logging.getMessage("GL.UnableToCreateObject", this.nameFromShaderType(type)); - Logging.error(msg); - throw new WWRuntimeException(msg); - } - - if (!this.compile(shader, source)) - { - // Get the info log before deleting the shader object. - String infoLog = GLES20.glGetShaderInfoLog(shader); - GLES20.glDeleteShader(shader); - - String msg = Logging.getMessage("GL.UnableToCompileShader", this.nameFromShaderType(type), infoLog); - Logging.error(msg); - throw new WWRuntimeException(msg); - } - - this.type = type; - this.shaderId = shader; - this.estimatedMemorySize = this.estimateMemorySize(source); - } - - public GpuShader(int type, int shaderId, long estimatedMemorySize) - { - if (type != GLES20.GL_VERTEX_SHADER && type != GLES20.GL_FRAGMENT_SHADER) - { - String msg = Logging.getMessage("generic.TypeIsInvalid", type); - Logging.error(msg); - throw new IllegalArgumentException(msg); - } - - if (shaderId <= 0) - { - String msg = Logging.getMessage("GL.GLObjectIsInvalid", shaderId); - Logging.error(msg); - throw new IllegalArgumentException(msg); - } - - if (estimatedMemorySize <= 0) - { - String msg = Logging.getMessage("generic.SizeIsInvalid", estimatedMemorySize); - Logging.error(msg); - throw new IllegalArgumentException(msg); - } - - this.type = type; - this.shaderId = shaderId; - this.estimatedMemorySize = estimatedMemorySize; - } - - public int getType() - { - return this.type; - } - - public int getShaderId() - { - return this.shaderId; - } - - public long getSizeInBytes() - { - return this.estimatedMemorySize; - } - - public void dispose() - { - if (this.shaderId != 0) - { - GLES20.glDeleteShader(this.shaderId); - this.shaderId = 0; - } - } - - protected boolean compile(int shader, String source) - { - int[] compileStatus = new int[1]; - - GLES20.glShaderSource(shader, source); - GLES20.glCompileShader(shader); - GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0); - - return compileStatus[0] == GLES20.GL_TRUE; - } - - protected long estimateMemorySize(String source) - { - return 2 * source.length(); - } - - protected String nameFromShaderType(int type) - { - if (type == GLES20.GL_VERTEX_SHADER) - { - return Logging.getMessage("term.VertexShader"); - } - else if (type == GLES20.GL_FRAGMENT_SHADER) - { - return Logging.getMessage("term.FragmentShader"); - } - - return null; - } + protected int type; + protected int shaderId; + protected long estimatedMemorySize; + + public GpuShader(int type, String source) + { + if (type != GLES20.GL_VERTEX_SHADER && type != GLES20.GL_FRAGMENT_SHADER) + { + String msg = Logging.getMessage("generic.TypeIsInvalid", type); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + if (WWUtil.isEmpty(source)) + { + String msg = Logging.getMessage("nullValue.SourceIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + int shader = GLES20.glCreateShader(type); + WorldWindowImpl.glCheckError("glCreateShader"); + if (shader <= 0) + { + String msg = Logging.getMessage("GL.UnableToCreateObject", this.nameFromShaderType(type)); + Logging.error(msg); + throw new WWRuntimeException(msg); + } + + if (!this.compile(shader, source)) + { + // Get the info log before deleting the shader object. + String infoLog = GLES20.glGetShaderInfoLog(shader); + WorldWindowImpl.glCheckError("glGetShaderInfoLog"); + GLES20.glDeleteShader(shader); + WorldWindowImpl.glCheckError("glDeleteShader"); + + String msg = Logging.getMessage("GL.UnableToCompileShader", this.nameFromShaderType(type), infoLog); + Logging.error(msg); + throw new WWRuntimeException(msg); + } + + this.type = type; + this.shaderId = shader; + this.estimatedMemorySize = this.estimateMemorySize(source); + } + + public GpuShader(int type, int shaderId, long estimatedMemorySize) + { + if (type != GLES20.GL_VERTEX_SHADER && type != GLES20.GL_FRAGMENT_SHADER) + { + String msg = Logging.getMessage("generic.TypeIsInvalid", type); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + if (shaderId <= 0) + { + String msg = Logging.getMessage("GL.GLObjectIsInvalid", shaderId); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + if (estimatedMemorySize <= 0) + { + String msg = Logging.getMessage("generic.SizeIsInvalid", estimatedMemorySize); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + this.type = type; + this.shaderId = shaderId; + this.estimatedMemorySize = estimatedMemorySize; + } + + public int getType() + { + return this.type; + } + + public int getShaderId() + { + return this.shaderId; + } + + public long getSizeInBytes() + { + return this.estimatedMemorySize; + } + + public void dispose() + { + if (this.shaderId != 0) + { + GLES20.glDeleteShader(this.shaderId); + WorldWindowImpl.glCheckError("glDeleteShader"); + this.shaderId = 0; + } + } + + protected boolean compile(int shader, String source) + { + int[] compileStatus = new int[1]; + + GLES20.glShaderSource(shader, source); + WorldWindowImpl.glCheckError("glShaderSource"); + GLES20.glCompileShader(shader); + WorldWindowImpl.glCheckError("glCompileShader"); + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0); + WorldWindowImpl.glCheckError("glGetShaderiv"); + + return compileStatus[0] == GLES20.GL_TRUE; + } + + protected long estimateMemorySize(String source) + { + return 2 * source.length(); + } + + protected String nameFromShaderType(int type) + { + if (type == GLES20.GL_VERTEX_SHADER) + { + return Logging.getMessage("term.VertexShader"); + } + else if (type == GLES20.GL_FRAGMENT_SHADER) + { + return Logging.getMessage("term.FragmentShader"); + } + + return null; + } } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/render/GpuTexture.java b/WorldWindAndroid/src/gov/nasa/worldwind/render/GpuTexture.java index d3e6c74..b47b607 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/render/GpuTexture.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/render/GpuTexture.java @@ -5,27 +5,29 @@ */ package gov.nasa.worldwind.render; +import android.graphics.Bitmap; +import android.graphics.RectF; +import android.opengl.GLES20; +import android.opengl.GLUtils; import gov.nasa.worldwind.Disposable; +import gov.nasa.worldwind.WorldWindowImpl; import gov.nasa.worldwind.cache.Cacheable; import gov.nasa.worldwind.geom.Matrix; import gov.nasa.worldwind.util.Logging; -import android.graphics.Bitmap; -import android.opengl.GLES20; -import android.opengl.GLUtils; /** * Edited By: Nicola Dorigatti, Trilogis - * + * * @author dcollins * @version $Id: GpuTexture.java 762 2012-09-07 00:22:58Z tgaskins $ */ public class GpuTexture implements Cacheable, Disposable { + public static GpuTexture createTexture(DrawContext dc, GpuTextureData textureData) { - if (dc == null) { - String msg = Logging.getMessage("nullValue.DrawContextIsNull"); - throw new IllegalArgumentException(msg); - } + return createTexture(textureData); + } + public static GpuTexture createTexture(GpuTextureData textureData) { if (textureData == null) { String msg = Logging.getMessage("nullValue.TextureDataIsNull"); Logging.error(msg); @@ -36,9 +38,9 @@ public static GpuTexture createTexture(DrawContext dc, GpuTextureData textureDat try { if (textureData.getBitmapData() != null) { - texture = doCreateFromBitmapData(dc, textureData); + texture = doCreateFromBitmapData(textureData); } else if (textureData.getCompressedData() != null) { - texture = doCreateFromCompressedData(dc, textureData); + texture = doCreateFromCompressedData(textureData); } else { String msg = Logging.getMessage("generic.TextureDataUnrecognized", textureData); Logging.error(msg); @@ -51,12 +53,13 @@ public static GpuTexture createTexture(DrawContext dc, GpuTextureData textureDat return texture; } - protected static GpuTexture doCreateFromBitmapData(DrawContext dc, GpuTextureData data) throws Exception { + protected static GpuTexture doCreateFromBitmapData(GpuTextureData data) throws Exception { Bitmap bitmap = data.getBitmapData().bitmap; int[] texture = new int[1]; try { GLES20.glGenTextures(1, texture, 0); + WorldWindowImpl.glCheckError("glGenTextures"); if (texture[0] <= 0) { String msg = Logging.getMessage("GL.UnableToCreateObject", Logging.getMessage("term.Texture")); Logging.error(msg); @@ -66,31 +69,42 @@ protected static GpuTexture doCreateFromBitmapData(DrawContext dc, GpuTextureDat // OpenGL ES provides support for non-power-of-two textures, including its associated mipmaps, provided that // the s and t wrap modes are both GL_CLAMP_TO_EDGE. GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture[0]); + WorldWindowImpl.glCheckError("glBindTexture"); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR_MIPMAP_LINEAR); + WorldWindowImpl.glCheckError("glTexParameteri"); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + WorldWindowImpl.glCheckError("glTexParameteri"); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + WorldWindowImpl.glCheckError("glTexParameteri"); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + WorldWindowImpl.glCheckError("glTexParameteri"); GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0); + WorldWindowImpl.glCheckError("texImage2D"); GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D); + WorldWindowImpl.glCheckError("glGenerateMipmap"); } catch (Exception e) { GLES20.glDeleteTextures(1, texture, 0); + WorldWindowImpl.glCheckError("glDeleteTextures"); throw e; } finally { GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); + WorldWindowImpl.glCheckError("glBindTexture"); } - return new GpuTexture(GLES20.GL_TEXTURE_2D, texture[0], bitmap.getWidth(), bitmap.getHeight(), data.getSizeInBytes(), createVerticalFlipTransform()); + return new GpuTexture(GLES20.GL_TEXTURE_2D, texture, bitmap.getWidth(), bitmap.getHeight(), data.getSizeInBytes(), createVerticalFlipTransform()); } - protected static GpuTexture doCreateFromCompressedData(DrawContext dc, GpuTextureData data) throws Exception { + protected static GpuTexture doCreateFromCompressedData(GpuTextureData data) throws Exception { int format = data.getCompressedData().format; GpuTextureData.MipmapData[] levelData = data.getCompressedData().levelData; + GpuTextureData.MipmapData[] alphaData = data.getCompressedData().alphaData; - int[] texture = new int[1]; + int[] texture = alphaData != null ? new int[2] : new int[1]; try { - GLES20.glGenTextures(1, texture, 0); + GLES20.glGenTextures(texture.length, texture, 0); + WorldWindowImpl.glCheckError("glGenTextures"); if (texture[0] <= 0) { String msg = Logging.getMessage("GL.UnableToCreateObject", Logging.getMessage("term.Texture")); Logging.error(msg); @@ -99,24 +113,55 @@ protected static GpuTexture doCreateFromCompressedData(DrawContext dc, GpuTextur // OpenGL ES provides support for non-power-of-two textures, including its associated mipmaps, provided that // the s and t wrap modes are both GL_CLAMP_TO_EDGE. + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + WorldWindowImpl.glCheckError("glActiveTexture"); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture[0]); + WorldWindowImpl.glCheckError("glBindTexture"); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, levelData.length > 1 ? GLES20.GL_LINEAR_MIPMAP_LINEAR : GLES20.GL_LINEAR); + WorldWindowImpl.glCheckError("glTexParameteri"); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + WorldWindowImpl.glCheckError("glTexParameteri"); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + WorldWindowImpl.glCheckError("glTexParameteri"); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + WorldWindowImpl.glCheckError("glTexParameteri"); for (int levelNum = 0; levelNum < levelData.length; levelNum++) { GpuTextureData.MipmapData level = levelData[levelNum]; GLES20.glCompressedTexImage2D(GLES20.GL_TEXTURE_2D, levelNum, format, level.width, level.height, 0, level.buffer.remaining(), level.buffer); + WorldWindowImpl.glCheckError("glCompressedTexImage2D"); + } + + if (alphaData != null) { + GLES20.glActiveTexture(GLES20.GL_TEXTURE1); + WorldWindowImpl.glCheckError("glActiveTexture"); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture[1]); + WorldWindowImpl.glCheckError("glBindTexture"); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, alphaData.length > 1 ? GLES20.GL_LINEAR_MIPMAP_LINEAR : GLES20.GL_LINEAR); + WorldWindowImpl.glCheckError("glTexParameteri"); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + WorldWindowImpl.glCheckError("glTexParameteri"); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + WorldWindowImpl.glCheckError("glTexParameteri"); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + WorldWindowImpl.glCheckError("glTexParameteri"); + + for (int levelNum = 0; levelNum < alphaData.length; levelNum++) { + GpuTextureData.MipmapData level = alphaData[levelNum]; + GLES20.glCompressedTexImage2D(GLES20.GL_TEXTURE_2D, levelNum, format, level.width, level.height, 0, level.buffer.remaining(), level.buffer); + WorldWindowImpl.glCheckError("glCompressedTexImage2D"); + } } } catch (Exception e) { - GLES20.glDeleteTextures(1, texture, 0); + GLES20.glDeleteTextures(texture.length, texture, 0); + WorldWindowImpl.glCheckError("glDeleteTextures"); throw e; } finally { GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); + WorldWindowImpl.glCheckError("glBindTexture"); } - return new GpuTexture(GLES20.GL_TEXTURE_2D, texture[0], levelData[0].width, levelData[0].height, data.getSizeInBytes(), createVerticalFlipTransform()); + return new GpuTexture(GLES20.GL_TEXTURE_2D, texture, levelData[0].width, levelData[0].height, data.getSizeInBytes(), createVerticalFlipTransform()); } protected static Matrix createVerticalFlipTransform() { @@ -125,12 +170,12 @@ protected static Matrix createVerticalFlipTransform() { // expect the coordinate origin to be in the lower left corner, and interpret textures as having their data // origin in the lower left corner, images loaded by the Android BitmapFactory must always be flipped // vertically. Flipping an image vertically is accomplished by multiplying scaling the t-coordinate by -1 then - // translating the t-coordinate by -1. We have pre-computed the product of the scaling and translation matrices + // translating the t-coordinate by 1. We have pre-computed the product of the scaling and translation matrices // and stored the result inline here to avoid unnecessary matrix allocations and multiplications. The matrix // below is equivalent to the following: // // Matrix scale = Matrix.fromIdentity().setScale(1, -1, 1); - // Matrix trans = Matrix.fromIdentity().setTranslation(0, -1, 0); + // Matrix trans = Matrix.fromIdentity().setTranslation(0, 1, 0); // Matrix internalTransform = Matrix.fromIdentity(); // internalTransform.multiplyAndSet(scale); // internalTransform.multiplyAndSet(trans); @@ -140,13 +185,13 @@ protected static Matrix createVerticalFlipTransform() { } protected int target; - protected int textureId; + protected int[] textureId; protected int width; protected int height; protected long estimatedMemorySize; protected Matrix internalTransform; - public GpuTexture(int target, int textureId, int width, int height, long estimatedMemorySize, Matrix texCoordMatrix) { + public GpuTexture(int target, int[] textureId, int width, int height, long estimatedMemorySize, Matrix texCoordMatrix) { if (target != GLES20.GL_TEXTURE_2D && target != GLES20.GL_TEXTURE_CUBE_MAP_NEGATIVE_X && target != GLES20.GL_TEXTURE_CUBE_MAP_NEGATIVE_Y && target != GLES20.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z && target != GLES20.GL_TEXTURE_CUBE_MAP_POSITIVE_X && target != GLES20.GL_TEXTURE_CUBE_MAP_POSITIVE_Y && target != GLES20.GL_TEXTURE_CUBE_MAP_POSITIVE_Z) { @@ -155,7 +200,7 @@ public GpuTexture(int target, int textureId, int width, int height, long estimat throw new IllegalArgumentException(msg); } - if (textureId <= 0) { + if (textureId.length < 1 || textureId[0] <= 0) { String msg = Logging.getMessage("GL.GLObjectIsInvalid", textureId); Logging.error(msg); throw new IllegalArgumentException(msg); @@ -192,7 +237,7 @@ public int getTarget() { } public int getTextureId() { - return this.textureId; + return this.textureId[0]; } public int getWidth() { @@ -208,12 +253,17 @@ public long getSizeInBytes() { } public void bind() { - GLES20.glBindTexture(this.target, this.textureId); + for (int i = 0; i < textureId.length; i++) { + GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i); + WorldWindowImpl.glCheckError("glActiveTexture"); + GLES20.glBindTexture(this.target, this.textureId[i]); + WorldWindowImpl.glCheckError("glBindTexture"); + } } public void dispose() { - int[] textures = new int[] { this.textureId }; - GLES20.glDeleteTextures(1, textures, 0); + GLES20.glDeleteTextures(textureId.length, textureId, 0); + WorldWindowImpl.glCheckError("glDeleteTextures"); } public void applyInternalTransform(DrawContext dc, Matrix matrix) { diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/render/GpuTextureData.java b/WorldWindAndroid/src/gov/nasa/worldwind/render/GpuTextureData.java index 4540014..f72a8dc 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/render/GpuTextureData.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/render/GpuTextureData.java @@ -5,14 +5,29 @@ */ package gov.nasa.worldwind.render; -import android.graphics.*; -import android.opengl.*; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.opengl.ETC1; +import android.opengl.ETC1Util; +import android.opengl.GLES20; +import android.opengl.GLUtils; +import gov.nasa.worldwind.WorldWindowImpl; import gov.nasa.worldwind.cache.Cacheable; -import gov.nasa.worldwind.util.*; +import gov.nasa.worldwind.util.Logging; +import gov.nasa.worldwind.util.OGLUtil; +import gov.nasa.worldwind.util.WWIO; +import gov.nasa.worldwind.util.WWUtil; +import gov.nasa.worldwind.util.dds.DDSCompressor; import gov.nasa.worldwind.util.dds.DDSTextureReader; +import gov.nasa.worldwind.util.dds.DXTCompressionAttributes; +import gov.nasa.worldwind.util.pkm.ETC1Compressor; -import java.io.*; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.List; /** * @author dcollins @@ -20,6 +35,8 @@ */ public class GpuTextureData implements Cacheable { + private static final int MAX_MIP_LEVELS = 12; + public static class BitmapData { public final Bitmap bitmap; @@ -41,8 +58,13 @@ public static class CompressedData { public final int format; public final MipmapData[] levelData; + public final MipmapData[] alphaData; + + public CompressedData(int format, MipmapData[] levelData) { + this(format, levelData, null); + } - public CompressedData(int format, MipmapData[] levelData) + public CompressedData(int format, MipmapData[] levelData, MipmapData[] alphaData) { if (levelData == null || levelData.length == 0) { @@ -53,6 +75,7 @@ public CompressedData(int format, MipmapData[] levelData) this.format = format; this.levelData = levelData; + this.alphaData = alphaData; } } @@ -91,7 +114,29 @@ public MipmapData(int width, int height, ByteBuffer buffer) } } - public static GpuTextureData createTextureData(Object source) + + /** + * Create GpuTextureData + * @param source Bitmap, InputStream, URL, or String path + * @param textureFormat Desired texture format. If input is uncompressed and format is compressed, compression will be performed + * @param useMipMaps Whether to load/create mipmaps + * @return The GpuTextureData + */ + public static GpuTextureData createTextureData(Object source, String textureFormat, boolean useMipMaps) { + if(source instanceof String) + return createTextureData(source, (String)source, textureFormat, useMipMaps); + return createTextureData(source, null, textureFormat, useMipMaps); + } + + /** + * Create GpuTextureData + * @param source Bitmap, InputStream, URL, or String path + * @param url Filename, if applicable. Used for format detection and alpha/mip location + * @param textureFormat Desired texture format. If input is uncompressed and format is compressed, compression will be performed + * @param useMipMaps Whether to load/create mipmaps + * @return The GpuTextureData + */ + public static GpuTextureData createTextureData(Object source, String url, String textureFormat, boolean useMipMaps) { if (WWUtil.isEmpty(source)) { @@ -113,7 +158,7 @@ public static GpuTextureData createTextureData(Object source) // Attempt to open the source as an InputStream. This handle URLs, Files, InputStreams, a String // containing a valid URL, a String path to a file on the local file system, and a String path to a // class path resource. - InputStream stream = WWIO.openStream(source); + InputStream stream = WWIO.openBufferedStream(source); try { if (stream != null) @@ -121,9 +166,7 @@ public static GpuTextureData createTextureData(Object source) // Wrap the stream in a BufferedInputStream to provide the mark/reset capability required to // avoid destroying the stream when it is read more than once. BufferedInputStream also improves // file read performance. - if (!(stream instanceof BufferedInputStream)) - stream = new BufferedInputStream(stream); - data = fromStream(stream); + data = fromStream(stream, url, textureFormat, useMipMaps); } } finally @@ -134,8 +177,7 @@ public static GpuTextureData createTextureData(Object source) } catch (Exception e) { - String msg = Logging.getMessage("GpuTextureFactory.TextureDataCreationFailed", source); - Logging.error(msg); + Logging.error(Logging.getMessage("GpuTextureFactory.TextureDataCreationFailed", source), e); } return data; @@ -143,30 +185,104 @@ public static GpuTextureData createTextureData(Object source) protected static final int DEFAULT_MARK_LIMIT = 1024; - protected static GpuTextureData fromStream(InputStream stream) - { - GpuTextureData data = null; - try - { - stream.mark(DEFAULT_MARK_LIMIT); - - DDSTextureReader ddsReader = new DDSTextureReader(); - data = ddsReader.read(stream); - if (data != null) - return data; - - stream.reset(); - - Bitmap bitmap = BitmapFactory.decodeStream(stream); - return bitmap != null ? new GpuTextureData(bitmap, estimateMemorySize(bitmap)) : null; - } - catch (IOException e) - { - // TODO - } - - return data; - } + protected static GpuTextureData fromStream(InputStream stream, String url, String textureFormat, boolean useMipMaps) throws IOException { + GpuTextureData data = null; + stream.mark(DEFAULT_MARK_LIMIT); + + if(textureFormat==null && url!=null) +// textureFormat = "image/"+WWIO.getFileExtension(url); + textureFormat = WWIO.makeMimeTypeForSuffix(WWIO.getSuffix(url)); + + if ("image/dds".equalsIgnoreCase(textureFormat) && url!=null && !url.toString().toLowerCase().endsWith("dds")) + { + if(WorldWindowImpl.DEBUG) + Logging.verbose("Compressing DDS texture " + url); + // Configure a DDS compressor to generate mipmaps based according to the 'useMipMaps' parameter, and + // convert the image URL to a compressed DDS format. + DXTCompressionAttributes attributes = DDSCompressor.getDefaultCompressionAttributes(); + attributes.setBuildMipmaps(useMipMaps); + DDSTextureReader ddsReader = new DDSTextureReader(); + return ddsReader.read(WWIO.getInputStreamFromByteBuffer(DDSCompressor.compressImageStream(stream, attributes))); + } + else if("image/dds".equalsIgnoreCase(textureFormat)) + { + if(WorldWindowImpl.DEBUG) + Logging.verbose("Loading DDS texture " + url); + DDSTextureReader ddsReader = new DDSTextureReader(); + return ddsReader.read(stream); + } + else if ("image/pkm".equalsIgnoreCase(textureFormat) && url!=null && !url.toString().toLowerCase().endsWith("pkm")) + { + if(WorldWindowImpl.DEBUG) + Logging.verbose("Compressing ETC1 texture " + url); + ETC1Util.ETC1Texture[] etc1tex = ETC1Compressor.compressImage(BitmapFactory.decodeStream(stream)); + + MipmapData mipmapData = new MipmapData(etc1tex[0].getWidth(), etc1tex[0].getHeight(), etc1tex[0].getData()); + MipmapData []alphaMipmap = etc1tex.length==1 ? null : + new MipmapData[] {new MipmapData(etc1tex[1].getWidth(), etc1tex[1].getHeight(), etc1tex[1].getData())}; + + return new GpuTextureData(ETC1.ETC1_RGB8_OES, new MipmapData[] {mipmapData}, alphaMipmap, etc1tex[0].getData().remaining()); + } + else if ("image/pkm".equalsIgnoreCase(textureFormat)) + { + if(WorldWindowImpl.DEBUG) + Logging.verbose("Loading ETC1 texture " + url); + + List colorData = new LinkedList(); + List alphaData = new LinkedList(); + + ETC1Util.ETC1Texture etc1tex = ETC1Util.createTexture(stream); + MipmapData mipmapData = new MipmapData(etc1tex.getWidth(), etc1tex.getHeight(), etc1tex.getData()); + colorData.add(mipmapData); + int dataSize = etc1tex.getData().limit(); + + MipmapData alphaLevel0 = readETC1(url.substring(0, url.lastIndexOf(".pkm")) + "_alpha.pkm"); + if(alphaLevel0 != null) { + alphaData.add(alphaLevel0); + dataSize += alphaLevel0.buffer.limit(); + } + + if(useMipMaps) { + for(int i=0; i>> 32)); + result = 31 * result + (this.ambient != null ? this.ambient.hashCode() : 0); + result = 31 * result + (this.diffuse != null ? this.diffuse.hashCode() : 0); + result = 31 * result + (this.specular != null ? this.specular.hashCode() : 0); + result = 31 * result + (this.emission != null ? this.emission.hashCode() : 0); + return result; + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/render/OrderedRenderable.java b/WorldWindAndroid/src/gov/nasa/worldwind/render/OrderedRenderable.java index c8fb518..56f4b12 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/render/OrderedRenderable.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/render/OrderedRenderable.java @@ -6,6 +6,7 @@ package gov.nasa.worldwind.render; import android.graphics.Point; +import gov.nasa.worldwind.layers.Layer; /** * @author tag @@ -13,6 +14,12 @@ */ public interface OrderedRenderable extends Renderable { + /** + * Layer this ordered renderable is in + * @return + */ + Layer getLayer(); + /** * Returns the ordered renderable's distance from the current view's eye point. Intended to be used only to sort a * list of ordered renderables according to eye distance, and only during frame generation when a view is active. diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/render/Path.java b/WorldWindAndroid/src/gov/nasa/worldwind/render/Path.java index 4b86d8f..ee9629a 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/render/Path.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/render/Path.java @@ -4,6 +4,7 @@ */ package gov.nasa.worldwind.render; +import gov.nasa.worldwind.*; import gov.nasa.worldwind.avlist.AVKey; import gov.nasa.worldwind.cache.GpuResourceCache; import gov.nasa.worldwind.cache.ShapeDataCache; @@ -20,14 +21,17 @@ import gov.nasa.worldwind.geom.Vec4; import gov.nasa.worldwind.globes.Globe; import gov.nasa.worldwind.terrain.Terrain; -import gov.nasa.worldwind.util.BufferUtil; -import gov.nasa.worldwind.util.Logging; +import gov.nasa.worldwind.util.*; + import java.nio.FloatBuffer; +import java.nio.IntBuffer; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import android.opengl.GLES20; +import static gov.nasa.worldwind.WorldWindowImpl.glCheckError; + // TODO: Measurement (getLength), Texture, lighting /** @@ -66,7 +70,7 @@ * Path picking includes information about which position dots are picked, in addition to the path itself. A position dot under the cursor is returned as an * Integer object in the PickedObject's AVList under they key AVKey.ORDINAL. Position dots intersecting the pick rectangle are returned as a List of Integer * objects in the PickedObject's AVList under the key AVKey.ORDINAL_LIST. - * + * * @author tag * @version $Id: Path.java 844 2012-10-11 00:35:07Z tgaskins $ */ @@ -113,7 +117,7 @@ public static interface PositionColors { * with 0 and increase by 1 for every originally specified position. For example, the first three path positions have ordinal values 0, 1, 2. *

    * The returned color's RGB components must not be premultiplied by its Alpha component. - * + * * @param position * the path position the color corresponds to. * @param ordinal @@ -140,17 +144,18 @@ protected static class PathData extends AbstractShapeData { * extruded, the base vertices are interleaved: Vcap, Vbase, Vcap, Vbase, ... */ protected FloatBuffer renderedPath; - // /** - // * Indices to the renderedPath identifying the vertices of the originally specified boundary - // * positions and their corresponding terrain point. This is used to draw vertical lines at those positions when - // * the path is extruded. - // */ - // protected IntBuffer polePositions; // identifies original positions and corresponding ground points - // /** - // * Indices to the renderedPath identifying the vertices of the originally specified boundary - // * positions. (Not their terrain points as well, as polePositions does.) - // */ - // protected IntBuffer positionPoints; // identifies the original positions in the rendered path. + /** + * Indices to the renderedPath identifying the vertices of the originally specified boundary + * positions and their corresponding terrain point. This is used to draw vertical lines at those positions when + * the path is extruded. + */ + protected IntBuffer polePositions; + /** + * Indices to the renderedPath identifying the vertices of the originally specified boundary + * positions. (Not their terrain points as well, as polePositions does.) + */ + protected IntBuffer positionPoints; + /** Indicates whether the rendered path has extrusion points in addition to path points. */ protected boolean hasExtrusionPoints; // true when the rendered path contains extrusion points /** @@ -172,7 +177,7 @@ public PathData(DrawContext dc, Path shape) { /** * The positions resulting from tessellating this path. If the path's attributes don't cause tessellation, then * the positions returned are those originally specified. - * + * * @return the positions computed by path tessellation. */ public List getTessellatedPositions() { @@ -186,7 +191,7 @@ public void setTessellatedPositions(ArrayList tessellatedPositions) { /** * Indicates the colors corresponding to each position in tessellatedPositions, or null if the path does not have per-position * colors. - * + * * @return the colors corresponding to each path position, or null if the path does not have * per-position colors. */ @@ -198,7 +203,7 @@ public List getTessellatedColors() { * Specifies the colors corresponding to each position in tessellatedPositions, or null to specify that the path does not have * per-position colors. The entries in the specified * list must have a one-to-one correspondence with the entries in tessellatedPositions. - * + * * @param tessellatedColors * the colors corresponding to each path position, or null if the path * does not have per-position colors. @@ -210,7 +215,7 @@ public void setTessellatedColors(ArrayList tessellatedColors) { /** * The Cartesian coordinates of the tessellated positions. If path verticals are enabled, this path also * contains the ground points corresponding to the path positions. - * + * * @return the Cartesian coordinates of the tessellated positions. */ public FloatBuffer getRenderedPath() { @@ -221,42 +226,42 @@ public void setRenderedPath(FloatBuffer renderedPath) { this.renderedPath = renderedPath; } - // /** - // * Returns a buffer of indices into the rendered path ({@link #renderedPath} that identify the originally - // * specified positions that remain after tessellation. These positions are those of the position dots, if - // * drawn. - // * - // * @return the path's originally specified positions that survived tessellation. - // */ - // public IntBuffer getPositionPoints() - // { - // return this.positionPoints; - // } - // - // public void setPositionPoints(IntBuffer posPoints) - // { - // this.positionPoints = posPoints; - // } - - // /** - // * Returns a buffer of indices into the rendered path ({@link #renderedPath} that identify the top and bottom - // * vertices of this path's vertical line segments. - // * - // * @return the path's pole positions. - // */ - // public IntBuffer getPolePositions() - // { - // return this.polePositions; - // } - // - // public void setPolePositions(IntBuffer polePositions) - // { - // this.polePositions = polePositions; - // } + /** + * Returns a buffer of indices into the rendered path ({@link #renderedPath} that identify the originally + * specified positions that remain after tessellation. These positions are those of the position dots, if + * drawn. + * + * @return the path's originally specified positions that survived tessellation. + */ + public IntBuffer getPositionPoints() + { + return this.positionPoints; + } + + public void setPositionPoints(IntBuffer posPoints) + { + this.positionPoints = posPoints; + } + + /** + * Returns a buffer of indices into the rendered path ({@link #renderedPath} that identify the top and bottom + * vertices of this path's vertical line segments. + * + * @return the path's pole positions. + */ + public IntBuffer getPolePositions() + { + return this.polePositions; + } + + public void setPolePositions(IntBuffer polePositions) + { + this.polePositions = polePositions; + } /** * Indicates whether this path is extruded and the extrusion points have been computed. - * + * * @return true if the path is extruded and the extrusion points are computed, otherwise false. */ public boolean hasExtrusionPoints() { @@ -270,7 +275,7 @@ public void setHasExtrusionPoints(boolean hasExtrusionPoints) { /** * Indicates the offset in number of floats to the first RGBA color tuple in renderedPath. This * returns 0 if renderedPath has no RGBA color tuples. - * + * * @return the offset in number of floats to the first RGBA color tuple in renderedPath. */ public int getColorOffset() { @@ -280,7 +285,7 @@ public int getColorOffset() { /** * Specifies the offset in number of floats to the first RGBA color tuple in renderedPath. Specify * 0 if renderedPath has no RGBA color tuples. - * + * * @param offset * the offset in number of floats to the first RGBA color tuple in renderedPath. */ @@ -290,7 +295,7 @@ public void setColorOffset(int offset) { /** * Indicates the stride in number of floats between the first element of consecutive vertices in renderedPath. - * + * * @return the stride in number of floats between vertices in in renderedPath. */ public int getVertexStride() { @@ -299,7 +304,7 @@ public int getVertexStride() { /** * Specifies the stride in number of floats between the first element of consecutive vertices in renderedPath. - * + * * @param stride * the stride in number of floats between vertices in in renderedPath. */ @@ -309,7 +314,7 @@ public void setVertexStride(int stride) { /** * Indicates the number of vertices in renderedPath. - * + * * @return the the number of verices in renderedPath. */ public int getVertexCount() { @@ -318,7 +323,7 @@ public int getVertexCount() { /** * Specifies the number of vertices in renderedPath. Specify 0 if renderedPath contains no vertices. - * + * * @param count * the the number of verices in renderedPath. */ @@ -612,6 +617,7 @@ protected PathData getCurrentPathData() { protected boolean showPositions = false; protected double showPositionsThreshold = DEFAULT_DRAW_POSITIONS_THRESHOLD; protected double showPositionsScale = DEFAULT_DRAW_POSITIONS_SCALE; + protected TextRenderer textRenderer; /** Creates a path with no positions. */ public Path() { @@ -621,7 +627,7 @@ public Path() { * Creates a path with specified positions. *

    * 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 @@ -637,7 +643,7 @@ public Path(Iterable positions) { * Creates a path with positions specified via a generic list. *

    * 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 @@ -657,7 +663,7 @@ public Path(Position.PositionList positions) { /** * Creates a path between two positions. - * + * * @param posA * the first position. * @param posB @@ -700,7 +706,7 @@ protected void reset() { /** * Returns this path's positions. - * + * * @return this path's positions. Will be null if no positions have been specified. */ public Iterable getPositions() { @@ -711,7 +717,7 @@ public Iterable getPositions() { * Specifies this path's positions, which replace this path's current positions, if any. *

    * Note: If fewer than two positions is specified, this path is not drawn. - * + * * @param positions * this path's positions. * @throws IllegalArgumentException @@ -734,7 +740,7 @@ public void setPositions(Iterable positions) { * Indicates the PositionColors that defines the RGBA color for each of this path's positions. A return value of null is valid and indicates * that this path's positions are colored according to its * ShapeAttributes. - * + * * @return this Path's PositionColors, or null if this path is colored according to its * ShapeAttributes. * @see #setPositionColors(gov.nasa.worldwind.render.Path.PositionColors) @@ -757,7 +763,7 @@ public PositionColors getPositionColors() { *

    * Specify null to disable position colors and draw this path's line and optional position dots according to its ShapeAttributes. This path's * position colors reference is null by default. - * + * * @param positionColors * the PositionColors that defines an RGBA color for each of this path's positions, or null to color this path's positions according * to its ShapeAttributes. @@ -772,7 +778,7 @@ public void setPositionColors(PositionColors positionColors) { /** * Indicates whether to extrude this path. Extruding the path extends a filled interior from the path to the * terrain. - * + * * @return true to extrude this path, otherwise false. * @see #setExtrude(boolean) */ @@ -783,7 +789,7 @@ public boolean isExtrude() { /** * Specifies whether to extrude this path. Extruding the path extends a filled interior from the path to the * terrain. - * + * * @param extrude * true to extrude this path, otherwise false. The default value is false. */ @@ -794,7 +800,7 @@ public void setExtrude(boolean extrude) { /** * Indicates whether this path is terrain following. - * + * * @return true if terrain following, otherwise false. * @see #setFollowTerrain(boolean) */ @@ -804,7 +810,7 @@ public boolean isFollowTerrain() { /** * Specifies whether this path is terrain following. - * + * * @param followTerrain * true if terrain following, otherwise false. The default value is false. */ @@ -820,7 +826,7 @@ public void setFollowTerrain(boolean followTerrain) { * cause the path to conform more closely to the path type but decrease performance. *

    * Note: The sub-segments number is ignored when the path follows terrain or when the path type is {@link AVKey#LINEAR}. - * + * * @return the number of sub-segments. * @see #setNumSubsegments(int) */ @@ -833,7 +839,7 @@ public int getNumSubsegments() { * cause the path to conform more closely to the path type but decrease performance. *

    * Note: The sub-segments number is ignored when the path follows terrain or when the path type is {@link AVKey#LINEAR}. - * + * * @param numSubsegments * the number of sub-segments. The default is 10. */ @@ -846,7 +852,7 @@ public void setNumSubsegments(int numSubsegments) { * Indicates the terrain conformance target when this path follows the terrain. The value indicates the maximum * number of pixels between which intermediate positions of a path segment -- the path portion between two specified * positions -- are computed. - * + * * @return the terrain conformance, in pixels. * @see #setTerrainConformance(double) */ @@ -858,7 +864,7 @@ public double getTerrainConformance() { * Specifies how accurately this path must adhere to the terrain when the path is terrain following. The value * specifies the maximum number of pixels between tessellation points. Lower values increase accuracy but decrease * performance. - * + * * @param terrainConformance * the number of pixels between tessellation points. */ @@ -869,7 +875,7 @@ public void setTerrainConformance(double terrainConformance) { /** * Indicates this paths path type. - * + * * @return the path type. * @see #setPathType(String) */ @@ -879,7 +885,7 @@ public String getPathType() { /** * Specifies this path's path type. Recognized values are {@link AVKey#GREAT_CIRCLE}, {@link AVKey#RHUMB_LINE} and {@link AVKey#LINEAR}. - * + * * @param pathType * the current path type. The default value is {@link AVKey#LINEAR}. */ @@ -890,7 +896,7 @@ public void setPathType(String pathType) { /** * Indicates whether to draw at each specified path position when this path is extruded. - * + * * @return true to draw the lines, otherwise false. * @see #setDrawVerticals(boolean) */ @@ -900,7 +906,7 @@ public boolean isDrawVerticals() { /** * Specifies whether to draw vertical lines at each specified path position when this path is extruded. - * + * * @param drawVerticals * true to draw the lines, otherwise false. The default value is true. */ @@ -911,7 +917,7 @@ public void setDrawVerticals(boolean drawVerticals) { /** * Indicates whether dots are drawn at the Path's original positions. - * + * * @return true if dots are drawn, otherwise false. */ public boolean isShowPositions() { @@ -921,7 +927,7 @@ public boolean isShowPositions() { /** * Specifies whether to draw dots at the original positions of the Path. The dot color and size are controlled by * the Path's outline material and scale attributes. - * + * * @param showPositions * true if dots are drawn at each original (not tessellated) position, otherwise false. */ @@ -933,7 +939,7 @@ public void setShowPositions(boolean showPositions) { * Indicates the scale factor controlling the size of dots drawn at this path's specified positions. The scale is * multiplied by the outline width given in this path's {@link ShapeAttributes} to determine the actual size of the * dots, in pixels. See {@link ShapeAttributes#setOutlineWidth(double)}. - * + * * @return the shape's draw-position scale. The default scale is 10. */ public double getShowPositionsScale() { @@ -944,7 +950,7 @@ public double getShowPositionsScale() { * Specifies the scale factor controlling the size of dots drawn at this path's specified positions. The scale is * multiplied by the outline width given in this path's {@link ShapeAttributes} to determine the actual size of the * dots, in pixels. See {@link ShapeAttributes#setOutlineWidth(double)}. - * + * * @param showPositionsScale * the new draw-position scale. */ @@ -954,7 +960,7 @@ public void setShowPositionsScale(double showPositionsScale) { /** * Indicates the eye distance from this shape's center beyond which position dots are not drawn. - * + * * @return the eye distance at which to enable or disable position dot drawing. The default is 1e6 meters, which * typically causes the dots to always be drawn. */ @@ -964,7 +970,7 @@ public double getShowPositionsThreshold() { /** * Specifies the eye distance from this shape's center beyond which position dots are not drawn. - * + * * @param showPositionsThreshold * the eye distance at which to enable or disable position dot drawing. */ @@ -978,6 +984,12 @@ public Sector getSector() { return this.sector; } + protected TextRenderer getTextRenderer(DrawContext dc) { + if(textRenderer==null) + textRenderer = new TextRenderer(dc); + return textRenderer; + } + @Override protected boolean mustDrawInterior() { return super.mustDrawInterior() && this.getCurrentPathData().hasExtrusionPoints; @@ -993,7 +1005,11 @@ protected boolean mustRegenerateGeometry(DrawContext dc) { if (this.getCurrentPathData().tessellatedPositions == null) return true; - if (dc.getVerticalExaggeration() != this.getCurrentPathData().getVerticalExaggeration()) return true; + if (dc.getVerticalExaggeration() != this.getCurrentPathData().getVerticalExaggeration()) { + if(WorldWindowImpl.DEBUG) + Logging.verbose("Path regenerating due to Vertical Exaggeration change"); + return true; + } // if ((this.getAltitudeMode() == null || AVKey.ABSOLUTE.equals(this.getAltitudeMode())) // && this.getCurrentPathData().getGlobeStateKey() != null @@ -1007,7 +1023,7 @@ protected boolean mustRegenerateGeometry(DrawContext dc) { * Indicates whether this Path's defining positions and the positions in between are located on the underlying * terrain. This returns true if this Path's altitude mode is WorldWind.CLAMP_TO_GROUND and the follow-terrain property is * true. Otherwise this returns false. - * + * * @return true if this Path's positions and the positions in between are located on the underlying * terrain, and false otherwise. */ @@ -1059,10 +1075,13 @@ protected boolean doMakeOrderedRenderable(DrawContext dc) { if (pathData.renderedPath.limit() > previousSize) this.clearCachedVbos(dc); - pathData.setExtent(this.computeExtent(pathData)); +// pathData.setExtent(this.computeExtent(pathData)); // If the shape is less that a pixel in size, don't render it. - if (this.getExtent() == null || dc.isSmall(this.getExtent(), 1)) return false; + if (this.getExtent() != null && dc.isSmall(this.getExtent(), 1)) { + + return false; + } if (!this.intersectsFrustum(dc)) return false; @@ -1128,6 +1147,56 @@ protected void applyModelviewProjectionMatrix(DrawContext dc) { } } + /** + * Establish the OpenGL state needed to draw this shape. + *

    + * A {@link gov.nasa.worldwind.render.AbstractShape.AbstractShapeData} must be current when this method is called. + * + * @param dc + * the current draw context. + */ + @Override + protected void beginDrawing(DrawContext dc) { + GpuProgram program = WWIO.getGpuProgram(dc.getGpuResourceCache(), programKey, R.raw.vertex_color_vert, R.raw.vertex_color_frag); + if (program == null) return; // Message already logged in getDefaultGpuProgram. + + // Bind this shape's gpu program as the current OpenGL program. + dc.setCurrentProgram(program); + program.bind(); + + // Enable the gpu program's vertexPoint attribute, if one exists. The data for this attribute is specified by + // each shape. + program.enableVertexAttribute("vertexPoint"); + + double opacity = 1d; + if(this.layer!=null) + opacity = layer.getOpacity(); + else if(dc.getCurrentLayer()!=null) + opacity = dc.getCurrentLayer().getOpacity(); + program.loadUniform1f("uOpacity", dc.isPickingMode() ? 1f : opacity); + + // Set the OpenGL state that this shape depends on. + GLES20.glDisable(GLES20.GL_CULL_FACE); + WorldWindowImpl.glCheckError("glDisable: GL_CULL_FACE"); + } + + /** + * Pop the state set in {@link #beginDrawing(DrawContext)}. + *

    + * A {@link gov.nasa.worldwind.render.AbstractShape.AbstractShapeData} must be current when this method is called. + * + * @param dc + * the current draw context. + */ + @Override + protected void endDrawing(DrawContext dc) { + GpuProgram program = dc.getCurrentProgram(); + if (program == null) return; // Message already logged in getDefaultGpuProgram via beginDrawing. + + program.disableVertexAttribute("vertexColor"); + super.endDrawing(dc); + } + /** * {@inheritDoc} *

    @@ -1142,7 +1211,6 @@ protected void doDrawOutline(DrawContext dc) { boolean isSurfacePath = this.isSurfacePath(); // Keep track for OpenGL state recovery. try { - if (isSurfacePath) GLES20.glDepthMask(false); int[] vboIds = this.getVboIds(dc); if (vboIds != null) this.doDrawOutlineVBO(dc, vboIds, this.getCurrentPathData()); @@ -1151,160 +1219,189 @@ protected void doDrawOutline(DrawContext dc) { Logging.warning(msg); } } finally { - if (isSurfacePath) GLES20.glDepthMask(true); // Restore the default depth mask. + if (isSurfacePath) { + GLES20.glDepthMask(true); // Restore the default depth mask. + glCheckError("glDepthMask"); + } } } protected void doDrawOutlineVBO(DrawContext dc, int[] vboIds, PathData pathData) { int attribLocation = dc.getCurrentProgram().getAttribLocation("vertexPoint"); if (attribLocation < 0) { - String msg = Logging.getMessage("GL.VertexAttributeIsMissing", "vertexPoint"); - Logging.warning(msg); + Logging.warning(Logging.getMessage("GL.VertexAttributeIsMissing", "vertexPoint")); } int stride = pathData.hasExtrusionPoints ? 2 * pathData.vertexStride : pathData.vertexStride; int count = pathData.hasExtrusionPoints ? pathData.vertexCount / 2 : pathData.vertexCount; - // boolean useVertexColors = !dc.isPickingMode() && pathData.tessellatedColors != null; + boolean useVertexColors = !dc.isPickingMode() && pathData.tessellatedColors != null; // Specify the data for the program's vertexPoint attribute, if one exists. This attribute is enabled in // beginRendering. Convert stride from number of elements to number of bytes. GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vboIds[0]); + glCheckError("glBindBuffer"); GLES20.glVertexAttribPointer(attribLocation, 3, GLES20.GL_FLOAT, false, 4 * stride, 0); + glCheckError("glVertexAttribPointer"); // Apply this path's per-position colors if we're in normal rendering mode (not picking) and this path's // positionColors is non-null. - // if (useVertexColors) - // { - // // Convert stride and offset from number of elements to number of bytes. - // gl.glEnableClientState(GL.GL_COLOR_ARRAY); - // gl.glColorPointer(4, GL.GL_FLOAT, 4 * stride, 4 * pathData.colorOffset); - // } + if (useVertexColors) + { + dc.getCurrentProgram().loadUniform1b("uUseVertexColor", true); + dc.getCurrentProgram().enableVertexAttribute("vertexColor"); + int colorLocation = dc.getCurrentProgram().getAttribLocation("vertexColor"); + if (colorLocation < 0) { + Logging.warning(Logging.getMessage("GL.VertexAttributeIsMissing", "vertexColor")); + } + // Convert stride and offset from number of elements to number of bytes. + GLES20.glVertexAttribPointer(colorLocation, 4, GLES20.GL_FLOAT, false, 4 * stride, 4*pathData.colorOffset); + glCheckError("glVertexAttribPointer"); + } GLES20.glDrawArrays(GLES20.GL_LINE_STRIP, 0, count); + glCheckError("glDrawArrays"); + + if (useVertexColors) + { + dc.getCurrentProgram().loadUniform1b("uUseVertexColor", false); + dc.getCurrentProgram().disableVertexAttribute("vertexColor"); + } - // if (useVertexColors) - // gl.glDisableClientState(GL.GL_COLOR_ARRAY); + if (pathData.hasExtrusionPoints && this.isDrawVerticals()) + this.drawVerticalOutlineVBO(dc, vboIds, pathData); - // if (pathData.hasExtrusionPoints && this.isDrawVerticals()) - // this.drawVerticalOutlineVBO(dc, vboIds, pathData); - // - // if (this.isShowPositions()) - // this.drawPointsVBO(dc, vboIds, pathData); + if (this.isShowPositions()) + { + this.drawPointsVBO(dc, vboIds, pathData); + drawPointLabels(dc, pathData, true); + } } - // /** - // * Draws vertical lines at this path's specified positions. - // * - // * @param dc the current draw context. - // * @param pathData the current globe-specific path data. - // */ - // protected void drawVerticalOutlineVBO(DrawContext dc, int[] vboIds, PathData pathData) - // { - // IntBuffer polePositions = pathData.polePositions; - // if (polePositions == null || polePositions.limit() < 1) - // return; - // - // GL gl = dc.getGL(); - // - // // Convert stride from number of elements to number of bytes. - // gl.glVertexPointer(3, GL.GL_FLOAT, 4 * pathData.vertexStride, 0); - // gl.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, vboIds[1]); - // gl.glDrawElements(GL.GL_LINES, polePositions.limit(), GL.GL_UNSIGNED_INT, 0); - // } + /** + * Draws vertical lines at this path's specified positions. + * + * @param dc the current draw context. + * @param pathData the current globe-specific path data. + */ + protected void drawVerticalOutlineVBO(DrawContext dc, int[] vboIds, PathData pathData) + { + IntBuffer polePositions = pathData.polePositions; + if (polePositions == null || polePositions.limit() < 1) + return; + + int attribLocation = dc.getCurrentProgram().getAttribLocation("vertexPoint"); + GLES20.glVertexAttribPointer(attribLocation, 3, GLES20.GL_FLOAT, false, 4 * pathData.vertexStride, 0); + glCheckError("glVertexAttribPointer"); + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, vboIds[1]); + glCheckError("glBindBuffer"); + GLES20.glDrawElements(GLES20.GL_LINES, polePositions.limit(), GLES20.GL_UNSIGNED_INT, 0); + glCheckError("glDrawElements"); + } + + /** + * Draws points at this path's specified positions. + *

    + * Note: when the draw context is in picking mode, this binds the current GL_ARRAY_BUFFER to 0 after using the + * currently bound GL_ARRAY_BUFFER to specify the vertex pointer. This does not restore GL_ARRAY_BUFFER to the its + * previous state. If the caller intends to use that buffer after this method returns, the caller must bind the + * buffer again. + * + * @param dc the current draw context. + * @param vboIds the ids of this shapes buffers. + * @param pathData the current globe-specific path data. + */ + protected void drawPointsVBO(DrawContext dc, int[] vboIds, PathData pathData) + { + double d = this.getDistanceMetric(dc, pathData); + if (d > this.getShowPositionsThreshold()) + return; + + IntBuffer posPoints = pathData.positionPoints; + if (posPoints == null || posPoints.limit() < 1) + return; + + // Convert stride from number of elements to number of bytes. + dc.getCurrentProgram().vertexAttribPointer("vertexPoint", 3, GLES20.GL_FLOAT, false, 4 * pathData.vertexStride, 0); + + if (dc.isPickingMode()) + { + dc.getCurrentProgram().loadUniform1b("uUseVertexColor", true); + dc.getCurrentProgram().enableVertexAttribute("vertexColor"); +// GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); +// dc.getCurrentProgram().vertexAttribPointer("vertexColor", 3, GLES20.GL_UNSIGNED_BYTE, false, 0, pickPositionColors); + } + else if (pathData.tessellatedColors != null) + { + dc.getCurrentProgram().loadUniform1b("uUseVertexColor", true); + dc.getCurrentProgram().enableVertexAttribute("vertexColor"); + // Apply this path's per-position colors if we're in normal rendering mode (not picking) and this path's + // positionColors is non-null. Convert the stride and offset from number of elements to number of bytes. + dc.getCurrentProgram().vertexAttribPointer("vertexColor", 4, GLES20.GL_FLOAT, false, 4*pathData.vertexStride, 4*pathData.colorOffset); + } + + this.prepareToDrawPoints(dc); + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, vboIds[2]); + glCheckError("glBindBuffer"); + GLES20.glDrawElements(GLES20.GL_POINTS, posPoints.limit(), GLES20.GL_UNSIGNED_INT, 0); + glCheckError("glDrawElements"); + + dc.getCurrentProgram().loadUniform1f("uPointSize", 1); + + // Restore the previous GL color array state. + if (dc.isPickingMode() || pathData.tessellatedColors != null) + { + dc.getCurrentProgram().loadUniform1b("uUseVertexColor", false); + dc.getCurrentProgram().disableVertexAttribute("vertexColor"); + } + } + + protected void drawPointLabels(DrawContext dc, PathData pathData, boolean ordinals) { + FloatBuffer path = pathData.getRenderedPath(); + final Vec4 point = new Vec4(), projected = new Vec4(); + final Position position = new Position(); + int stride = pathData.hasExtrusionPoints ? pathData.vertexStride*2 : pathData.vertexStride; + int index = 0; + + TextRenderer textRenderer = getTextRenderer(dc); + textRenderer.beginDrawing(); + + for(int i=0; i - // * Note: when the draw context is in picking mode, this binds the current GL_ARRAY_BUFFER to 0 after using the - // * currently bound GL_ARRAY_BUFFER to specify the vertex pointer. This does not restore GL_ARRAY_BUFFER to the its - // * previous state. If the caller intends to use that buffer after this method returns, the caller must bind the - // * buffer again. - // * - // * @param dc the current draw context. - // * @param vboIds the ids of this shapes buffers. - // * @param pathData the current globe-specific path data. - // */ - // protected void drawPointsVBO(DrawContext dc, int[] vboIds, PathData pathData) - // { - // double d = this.getDistanceMetric(dc, pathData); - // if (d > this.getShowPositionsThreshold()) - // return; - // - // IntBuffer posPoints = pathData.positionPoints; - // if (posPoints == null || posPoints.limit() < 1) - // return; - // - // GL gl = dc.getGL(); - // - // // Convert stride from number of elements to number of bytes. - // gl.glVertexPointer(3, GL.GL_FLOAT, 4 * pathData.vertexStride, 0); - // - // if (dc.isPickingMode()) - // { - // gl.glEnableClientState(GL.GL_COLOR_ARRAY); - // gl.glBindBuffer(GL.GL_ARRAY_BUFFER, 0); - // gl.glColorPointer(3, GL.GL_UNSIGNED_BYTE, 0, pickPositionColors); - // } - // else if (pathData.tessellatedColors != null) - // { - // // Apply this path's per-position colors if we're in normal rendering mode (not picking) and this path's - // // positionColors is non-null. Convert the stride and offset from number of elements to number of bytes. - // gl.glEnableClientState(GL.GL_COLOR_ARRAY); - // gl.glColorPointer(4, GL.GL_FLOAT, 4 * pathData.vertexStride, 4 * pathData.colorOffset); - // } - // - // this.prepareToDrawPoints(dc); - // gl.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, vboIds[2]); - // gl.glDrawElements(GL.GL_POINTS, posPoints.limit(), GL.GL_UNSIGNED_INT, 0); - // - // // Restore the previous GL point state. - // gl.glPointSize(1f); - // gl.glDisable(GL.GL_POINT_SMOOTH); - // - // // Restore the previous GL color array state. - // if (dc.isPickingMode() || pathData.tessellatedColors != null) - // gl.glDisableClientState(GL.GL_COLOR_ARRAY); - // } - // - // protected void prepareToDrawPoints(DrawContext dc) - // { - // GL gl = dc.getGL(); - // - // if (dc.isPickingMode()) - // { - // // During picking, compute the GL point size as the product of the active outline width and the show - // // positions scale, plus the positive difference (if any) between the outline pick width and the outline - // // width. During picking, the outline width is set to the larger of the outline width and the outline pick - // // width. We need to adjust the point size accordingly to ensure that the points are not covered by the - // // larger outline width. We add the difference between the normal and pick widths rather than scaling the - // // pick width by the show positions scale, because the latter produces point sizes that are too large, and - // // obscure the other nearby points. - // ShapeAttributes activeAttrs = this.getActiveAttributes(); - // double deltaWidth = activeAttrs.getOutlineWidth() < this.getOutlinePickWidth() - // ? this.getOutlinePickWidth() - activeAttrs.getOutlineWidth() : 0; - // gl.glPointSize((float) (this.getShowPositionsScale() * activeAttrs.getOutlineWidth() + deltaWidth)); - // } - // else - // { - // // During normal rendering mode, compute the GL point size as the product of the active outline width and - // // the show positions scale. This computation is consistent with the documentation for the methods - // // setShowPositionsScale and getShowPositionsScale. - // gl.glPointSize((float) (this.getShowPositionsScale() * this.getActiveAttributes().getOutlineWidth())); - // } - // - // // Enable point smoothing both in picking mode and normal rendering mode. Normally, we do not enable smoothing - // // during picking, because GL uses semi-transparent fragments to give the point a round and anti-aliased - // // appearance, and semi-transparent pixels do not correspond to this Path's pickable color. Since blending is - // // not enabled during picking but the alpha test is, this has the effect of producing a rounded point in the - // // pick buffer that has a sharp transition between the Path's pick color and the other fragment colors. Without - // // this state enabled, position points display as squares in the pick buffer. - // gl.glEnable(GL.GL_POINT_SMOOTH); - // gl.glHint(GL.GL_POINT_SMOOTH_HINT, GL.GL_NICEST); - // } /** * Draws this path's interior when the path is extruded. - * + * * @param dc * the current draw context. */ @@ -1327,14 +1424,17 @@ protected void doDrawInteriorVBO(DrawContext dc, int[] vboIds, PathData pathData // Specify the data for the program's vertexPoint attribute, if one exists. This attribute is enabled in // beginRendering. Convert stride from number of elements to number of bytes. GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vboIds[0]); + glCheckError("glBindBuffer"); GLES20.glVertexAttribPointer(attribLocation, 3, GLES20.GL_FLOAT, false, 4 * pathData.vertexStride, 0); + glCheckError("glVertexAttribPointer"); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, pathData.vertexCount); + glCheckError("glDrawArrays"); } /** * Computes the shape's model-coordinate path from a list of positions. Applies the path's terrain-conformance * settings. Adds extrusion points -- those on the ground -- when the path is extruded. - * + * * @param dc * the current draw context. * @param positions @@ -1361,7 +1461,7 @@ protected void computePath(DrawContext dc, List positions, PathData pa * Computes a terrain-conforming, model-coordinate path from a list of positions, using either a specified altitude * or the altitudes in the specified positions. Adds extrusion points -- those on the ground -- when the path is * extruded and the specified single altitude is not 0. - * + * * @param dc * the current draw context. * @param positions @@ -1415,7 +1515,7 @@ protected FloatBuffer computePointsRelativeToTerrain(DrawContext dc, List posit /** * Computes a point on a path and adds it to the renderable geometry. Used to generate extrusion vertices. - * + * * @param dc * the current draw context. * @param position @@ -1585,7 +1685,7 @@ protected void appendTerrainPoint(DrawContext dc, Position position, float[] col /** * Generates positions defining this path with path type and terrain-conforming properties applied. Builds the * path's tessellatedPositions and polePositions fields. - * + * * @param pathData * the current globe-specific path data. * @param dc @@ -1604,42 +1704,41 @@ protected void makeTessellatedPositions(DrawContext dc, PathData pathData) { if (pathData.tessellatedColors != null) pathData.tessellatedColors.clear(); } - // if (pathData.polePositions == null || pathData.polePositions.capacity() < this.numPositions * 2) - // pathData.polePositions = BufferUtil.newIntBuffer(this.numPositions * 2); - // else - // pathData.polePositions.clear(); - // - // if (pathData.positionPoints == null || pathData.positionPoints.capacity() < this.numPositions) - // pathData.positionPoints = BufferUtil.newIntBuffer(this.numPositions); - // else - // pathData.positionPoints.clear(); + if (pathData.polePositions == null || pathData.polePositions.capacity() < this.numPositions * 2) + pathData.polePositions = BufferUtil.newIntBuffer(this.numPositions * 2); + else + pathData.polePositions.clear(); + + if (pathData.positionPoints == null || pathData.positionPoints.capacity() < this.numPositions) + pathData.positionPoints = BufferUtil.newIntBuffer(this.numPositions); + else + pathData.positionPoints.clear(); this.makePositions(dc, pathData); pathData.tessellatedPositions.trimToSize(); - // pathData.polePositions.flip(); - // pathData.positionPoints.flip(); + pathData.polePositions.flip(); + pathData.positionPoints.flip(); if (pathData.tessellatedColors != null) pathData.tessellatedColors.trimToSize(); } - // /** - // * Computes this Path's distance from the eye point, for use in determining when to show positions points. The value - // * returned is only an approximation because the eye distance varies along the path. - // * - // * @param dc the current draw context. - // * @param pathData this path's current shape data. - // * - // * @return the distance of the shape from the eye point. If the eye distance cannot be computed, the eye position's - // * elevation is returned instead. - // */ - // protected double getDistanceMetric(DrawContext dc, PathData pathData) - // { - // if (pathData.getExtent() != null) - // return pathData.getExtent().distanceTo(dc.getView().getEyePoint()); - // - // return dc.getView().getEyePosition(dc.getGlobe()).elevation; - // } + /** + * Computes this Path's distance from the eye point, for use in determining when to show positions points. The value + * returned is only an approximation because the eye distance varies along the path. + * + * @param dc the current draw context. + * @param pathData this path's current shape data. + * + * @return the distance of the shape from the eye point. If the eye distance cannot be computed, the eye position's + * elevation is returned instead. + */ + protected double getDistanceMetric(DrawContext dc, PathData pathData) + { + return pathData.getExtent() != null + ? WWMath.computeDistanceFromEye(dc, pathData.getExtent()) + : dc.getView().getEyePosition().elevation; + } protected void makePositions(DrawContext dc, PathData pathData) { Iterator iter = this.positions.iterator(); @@ -1673,7 +1772,7 @@ protected void makePositions(DrawContext dc, PathData pathData) { * Adds a position to this path's tessellatedPositions list. If the specified color is not null, this adds the color to this * path's tessellatedColors list. If the specified * ordinal is not null, this adds the position's index to the polePositions and positionPoints index buffers. - * + * * @param pos * the position to add. * @param color @@ -1687,60 +1786,66 @@ protected void makePositions(DrawContext dc, PathData pathData) { * the current globe-specific path data. */ protected void addTessellatedPosition(Position pos, Color color, Integer ordinal, PathData pathData) { - // if (ordinal != null) - // { - // // NOTE: Assign these indices before adding the new position to the tessellatedPositions list. - // int index = pathData.tessellatedPositions.size() * 2; - // pathData.polePositions.put(index).put(index + 1); - // - // if (pathData.hasExtrusionPoints) - // pathData.positionPoints.put(index); - // else - // pathData.positionPoints.put(pathData.tessellatedPositions.size()); - // } + if (ordinal != null) + { + // NOTE: Assign these indices before adding the new position to the tessellatedPositions list. + int index = pathData.tessellatedPositions.size() * 2; + pathData.polePositions.put(index).put(index + 1); - pathData.tessellatedPositions.add(pos); // be sure to do the add after the pole position is set + if (pathData.hasExtrusionPoints) + pathData.positionPoints.put(index); + else + pathData.positionPoints.put(pathData.tessellatedPositions.size()); + } - if (color != null) pathData.tessellatedColors.add(color); - } + pathData.tessellatedPositions.add(pos); // be sure to do the add after the pole position is set - // /** - // * Returns the Path position corresponding index. This returns null if the index does not correspond to an original - // * position. - // * - // * @param positionIndex the position's index. - // * - // * @return the Position corresponding to the specified index. - // */ - // protected Position getPosition(int positionIndex) - // { - // PathData pathData = this.getCurrentPathData(); - // // Get an index into the tessellatedPositions list. - // int index = pathData.positionPoints.get(positionIndex); - // // Return the originally specified position, which is stored in the tessellatedPositions list. - // return (index >= 0 && index < pathData.tessellatedPositions.size()) ? - // pathData.tessellatedPositions.get(index) : null; - // } - // - // /** - // * Returns the ordinal number corresponding to the position. This returns null if the position index does not - // * correspond to an original position. - // * - // * @param positionIndex the position's index. - // * - // * @return the ordinal number corresponding to the specified position index. - // */ - // protected Integer getOrdinal(int positionIndex) - // { - // return positionIndex; - // } + if (color != null) + pathData.tessellatedColors.add(color); + } + + /** + * Returns the Path position corresponding index. This returns null if the index does not correspond to an original + * position. + * + * @param positionIndex the position's index. + * + * @return the Position corresponding to the specified index. + */ + protected Position getPosition(int positionIndex) + { + PathData pathData = this.getCurrentPathData(); + // Get an index into the tessellatedPositions list. + int index = pathData.positionPoints.get(positionIndex); + // Return the originally specified position, which is stored in the tessellatedPositions list. + return (index >= 0 && index < pathData.tessellatedPositions.size()) ? + pathData.tessellatedPositions.get(index) : null; + } + + /** + * Returns the ordinal number corresponding to the position. This returns null if the position index does not + * correspond to an original position. + * + * @param positionIndex the position's index. + * + * @return the ordinal number corresponding to the specified position index. + */ + protected Integer getOrdinal(int positionIndex) + { + IntBuffer positionPoints = getCurrentPathData().getPositionPoints(); + for(int i=0; inull if this path's positionColors property * is null. This returns white if a color cannot be determined * for the specified position and ordinal. - * + * * @param pos * the path position the color corresponds to. * @param ordinal @@ -1761,7 +1866,7 @@ protected boolean isSmall(DrawContext dc, Vec4 ptA, Vec4 ptB, int numPixels) { /** * Determines whether the segment between two path positions is visible. - * + * * @param dc * the current draw context. * @param posA @@ -1795,7 +1900,7 @@ protected boolean isSegmentVisible(DrawContext dc, Position posA, Position posB, /** * Creates the interior segment positions to adhere to the current path type and terrain-following settings. - * + * * @param dc * the current draw context. * @param posA @@ -1876,7 +1981,7 @@ protected void makeSegment(DrawContext dc, Position posA, Position posB, Vec4 pt /** * Computes the approximate model-coordinate, great-circle length between two positions. - * + * * @param dc * the current draw context. * @param posA @@ -1899,7 +2004,7 @@ protected double computeSegmentLength(DrawContext dc, Position posA, Position po /** * Computes this path's reference center. - * + * * @param dc * the current draw context. * @return the computed reference center, or null if it cannot be computed. @@ -1917,7 +2022,7 @@ protected void computeReferenceCenter(DrawContext dc) { * Computes the minimum distance between this Path and the eye point. *

    * A {@link gov.nasa.worldwind.render.AbstractShape.AbstractShapeData} must be current when this method is called. - * + * * @param dc * the draw context. * @param pathData @@ -1948,7 +2053,7 @@ protected double computeEyeDistance(DrawContext dc, PathData pathData) { /** * Computes the path's bounding box from the current rendering path. Assumes the rendering path is up-to-date. - * + * * @param current * the current data for this shape. * @return the computed extent. @@ -1983,7 +2088,7 @@ public Extent getExtent(Globe globe, double verticalExaggeration) { /** * Computes the path's reference position. The position returned is the center-most ordinal position in the path's * specified positions. - * + * * @return the computed reference position. */ public Position getReferencePosition() { @@ -1991,48 +2096,61 @@ public Position getReferencePosition() { } protected void fillVBO(DrawContext dc) { - PathData pathData = this.getCurrentPathData(); - int numIds = this.isShowPositions() ? 3 : pathData.hasExtrusionPoints && this.isDrawVerticals() ? 2 : 1; - - int[] vboIds = (int[]) dc.getGpuResourceCache().get(pathData.getVboCacheKey()); - if (vboIds != null && vboIds.length != numIds) { - this.clearCachedVbos(dc); - vboIds = null; - } - - int vSize = pathData.renderedPath.limit() * 4; - int iSize = pathData.hasExtrusionPoints && this.isDrawVerticals() ? pathData.tessellatedPositions.size() * 2 * 4 : 0; - if (this.isShowPositions()) iSize += pathData.tessellatedPositions.size(); - - if (vboIds == null) { - vboIds = new int[numIds]; - GLES20.glGenBuffers(vboIds.length, vboIds, 0); - dc.getGpuResourceCache().put(pathData.getVboCacheKey(), vboIds, GpuResourceCache.VBO_BUFFERS, vSize + iSize); - } - - try { - FloatBuffer vb = pathData.renderedPath; - GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vboIds[0]); - GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vb.limit() * 4, vb.rewind(), GLES20.GL_STATIC_DRAW); - - // if (pathData.hasExtrusionPoints && this.isDrawVerticals()) - // { - // IntBuffer ib = pathData.polePositions; - // GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, vboIds[1]); - // GLES20.glBufferData(GLES20.GL_ELEMENT_ARRAY_BUFFER, ib.limit() * 4, ib.rewind(), GLES20.GL_STATIC_DRAW); - // } - - // if (this.isShowPositions()) - // { - // IntBuffer ib = pathData.positionPoints; - // GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, vboIds[2]); - // GLES20.glBufferData(GLES20.GL_ELEMENT_ARRAY_BUFFER, ib.limit() * 4, ib.rewind(), GLES20.GL_STATIC_DRAW); - // } - } finally { - GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); - GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0); - } - } + PathData pathData = this.getCurrentPathData(); + int numIds = this.isShowPositions() ? 3 : pathData.hasExtrusionPoints && this.isDrawVerticals() ? 2 : 1; + + int[] vboIds = (int[]) dc.getGpuResourceCache().get(pathData.getVboCacheKey()); + if (vboIds != null && vboIds.length != numIds) { + this.clearCachedVbos(dc); + vboIds = null; + } + + int vSize = pathData.renderedPath.limit() * 4; +// int iSize = pathData.hasExtrusionPoints && this.isDrawVerticals() ? pathData.tessellatedPositions.size() * 2 * 4 : 0; +// if (this.isShowPositions()) iSize += pathData.tessellatedPositions.size(); + + int iSize = pathData.hasExtrusionPoints && this.isDrawVerticals() ? pathData.polePositions.limit() * 4 : 0; + if (this.isShowPositions()) iSize += pathData.positionPoints.limit() * 4; + + + if (vboIds == null) { + vboIds = new int[numIds]; + GLES20.glGenBuffers(vboIds.length, vboIds, 0); + glCheckError("glGenBuffers"); + dc.getGpuResourceCache().put(pathData.getVboCacheKey(), vboIds, GpuResourceCache.VBO_BUFFERS, vSize + iSize); + } + + try { + FloatBuffer vb = pathData.renderedPath; + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vboIds[0]); + glCheckError("glBindBuffer"); + GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vb.limit() * 4, vb.rewind(), GLES20.GL_STATIC_DRAW); + glCheckError("glBufferData"); + + if (pathData.hasExtrusionPoints && this.isDrawVerticals()) + { + IntBuffer ib = pathData.polePositions; + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, vboIds[1]); + glCheckError("glBindBuffer"); + GLES20.glBufferData(GLES20.GL_ELEMENT_ARRAY_BUFFER, ib.limit() * 4, ib.rewind(), GLES20.GL_STATIC_DRAW); + glCheckError("glBufferData"); + } + + if (this.isShowPositions()) + { + IntBuffer ib = pathData.positionPoints; + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, vboIds[2]); + glCheckError("glBindBuffer"); + GLES20.glBufferData(GLES20.GL_ELEMENT_ARRAY_BUFFER, ib.limit() * 4, ib.rewind(), GLES20.GL_STATIC_DRAW); + glCheckError("glBufferData"); + } + } finally { + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + glCheckError("glBindBuffer"); + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0); + glCheckError("glBindBuffer"); + } + } @Override public List intersect(Line line, Terrain terrain) throws InterruptedException // TODO diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/render/PreRenderable.java b/WorldWindAndroid/src/gov/nasa/worldwind/render/PreRenderable.java new file mode 100644 index 0000000..6360282 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/render/PreRenderable.java @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2012 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ + +package gov.nasa.worldwind.render; + +/** + * @author tag + * @version $Id: PreRenderable.java 1171 2013-02-11 21:45:02Z dcollins $ + */ +public interface PreRenderable +{ + void preRender(DrawContext dc); +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/render/ShapeAttributes.java b/WorldWindAndroid/src/gov/nasa/worldwind/render/ShapeAttributes.java index d394c34..90ee0e1 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/render/ShapeAttributes.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/render/ShapeAttributes.java @@ -61,6 +61,14 @@ */ public interface ShapeAttributes { + /** + * Returns a new ShapeAttributes instance of the same type as this ShapeAttributes who's properties are configured + * exactly as this ShapeAttributes. + * + * @return a copy of this ShapeAttributes. + */ + ShapeAttributes copy(); + /** * Sets this bundle's attributes to those of the specified bundle. This does not retain any reference to the * specified attributes, or modify them in any way. The specified bundle's attributes are copied into this bundle. @@ -141,6 +149,7 @@ public interface ShapeAttributes * * @see #setInteriorColor(Color) */ + @Deprecated Color getInteriorColor(); /** @@ -153,6 +162,7 @@ public interface ShapeAttributes * @throws IllegalArgumentException if the color is null. * @see #getInteriorColor() */ + @Deprecated void setInteriorColor(Color color); /** @@ -182,6 +192,7 @@ public interface ShapeAttributes * * @see #setOutlineColor(Color) */ + @Deprecated Color getOutlineColor(); /** @@ -194,6 +205,7 @@ public interface ShapeAttributes * @throws IllegalArgumentException if the color is null. * @see #getOutlineColor() */ + @Deprecated void setOutlineColor(Color color); /** @@ -217,4 +229,141 @@ public interface ShapeAttributes * @see #getOutlineWidth() */ void setOutlineWidth(double width); + + /** + * Indicates the material properties of the shape's interior. If lighting is applied to the shape, this indicates + * the interior's ambient, diffuse, and specular colors, its shininess, and the color of any emitted light. + * Otherwise, the material's diffuse color indicates the shape's constant interior color. + * + * @return the material applied to the shape's interior. + * + * @see #setInteriorMaterial(Material) + */ + Material getInteriorMaterial(); + + /** + * Specifies the material properties of the shape's interior. If lighting is applied to the shape, this specifies + * the interior's ambient, diffuse, and specular colors, its shininess, and the color of any emitted light. + * Otherwise, the material's diffuse color specifies the shape's constant interior color. + * + * @param material the material to apply to the shape's interior. + * + * @throws IllegalArgumentException if material is null. + * @see #getInteriorMaterial() + */ + void setInteriorMaterial(Material material); + + /** + * Indicates the material properties of the shape's outline. If lighting is applied to the shape, this indicates the + * outline's ambient, diffuse, and specular colors, its shininess, and the color of any emitted light. Otherwise, + * the material's diffuse color indicates the shape's constant outline color. + * + * @return the material applied to the shape's outline. + * + * @see #setOutlineMaterial(Material) + */ + Material getOutlineMaterial(); + + /** + * Specifies the material properties of the shape's outline. If lighting is applied to the shape, this specifies the + * outline's ambient, diffuse, and specular colors, its shininess, and the color of any emitted light. Otherwise, + * the material's diffuse color specifies as the shape's constant outline color. + * + * @param material the material to apply to the shape's outline. + * + * @throws IllegalArgumentException if material is null. + * @see #getOutlineMaterial() + */ + void setOutlineMaterial(Material material); + + /** + * Indicates the opacity of the shape's interior as a floating-point value in the range 0.0 to 1.0. + * + * @return the interior opacity as a floating-point value from 0.0 to 1.0. + * + * @see #setInteriorOpacity(double) + */ + double getInteriorOpacity(); + + /** + * Specifies the opacity of the shape's interior as a floating-point value in the range 0.0 to 1.0. A value of 1.0 + * specifies a completely opaque interior, and 0.0 specifies a completely transparent interior. Values in between + * specify a partially transparent interior. + * + * @param opacity the interior opacity as a floating-point value from 0.0 to 1.0. + * + * @throws IllegalArgumentException if opacity is less than 0.0 or greater than 1.0. + * @see #getInteriorOpacity() + */ + void setInteriorOpacity(double opacity); + + /** + * Indicates the opacity of the shape's outline as a floating-point value in the range 0.0 to 1.0. + * + * @return the outline opacity as a floating-point value from 0.0 to 1.0. + * + * @see #setOutlineOpacity(double) + */ + double getOutlineOpacity(); + + /** + * Specifies the opacity of the shape's outline as a floating-point value in the range 0.0 to 1.0. A value of 1.0 + * specifies a completely opaque outline, and 0.0 specifies a completely transparent outline. Values in between + * specify a partially transparent outline. + * + * @param opacity the outline opacity as a floating-point value from 0.0 to 1.0. + * + * @throws IllegalArgumentException if opacity is less than 0.0 or greater than 1.0. + * @see #getOutlineOpacity() + */ + void setOutlineOpacity(double opacity); + + /** + * Indicates the image source that is applied as a texture to the shape's interior. + * + * @return the source of the shape's texture, either a {@link String} path, a {@link java.net.URL}, a {@link + * java.awt.image.BufferedImage}, or null. + * + * @see #setImageSource(Object) + */ + Object getImageSource(); + + /** + * Specifies the image source to apply as a texture to the shape's interior, or null to specify that + * the shape should not have a texture. When not null, the texture replaces the shape's interior + * material. The source type may be one of the following:

    • {@link String} containing a path to a local file, + * or a resource on the classpath.
    • {@link java.net.URL}
    • {@link java.awt.image.BufferedImage}
    • + *
    • null
    If the image source is a file or a URL, it is read only when the + * shape is rendered. + * + * @param imageSource the source of the shape's texture, either a String path, a URL, a + * BufferedImage, or null. + * + * @see #getImageSource() + */ + void setImageSource(Object imageSource); + + /** + * Indicates the amount the shape's texture is scaled by as a floating-point value. + * + * @return the amount the shape's texture is scaled by as a floating-point value. This value is always greater + * than zero. + * + * @see #setImageScale(double) + */ + double getImageScale(); + + /** + * Specifies the amount to scale the shape's texture as a floating-point value. A value of 1.0 specifies that the + * texture should be applied without any scaling, a value greater than 1.0 specifies that the texture should be + * magnified, and a value less than 1.0 specifies that the texture should be minified. For example, a scale of 2.0 + * magnifies the texture by a factor of 2x. + * + * @param scale the amount to scale the shape's texture as a floating-point value. + * + * @throws IllegalArgumentException if scale is less than or equal to zero. + * @see #getImageScale() + * @see #setImageSource(Object) + */ + void setImageScale(double scale); } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/render/SurfaceImage.java b/WorldWindAndroid/src/gov/nasa/worldwind/render/SurfaceImage.java index 923e458..1716d0b 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/render/SurfaceImage.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/render/SurfaceImage.java @@ -130,7 +130,7 @@ protected GpuTexture loadGpuTexture(DrawContext dc) { GpuTexture texture = null; - GpuTextureData textureData = GpuTextureData.createTextureData(this.imagePath); + GpuTextureData textureData = GpuTextureData.createTextureData(this.imagePath, this.imagePath, null, true); if (textureData != null) { texture = GpuTexture.createTexture(dc, textureData); diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/render/SurfaceObject.java b/WorldWindAndroid/src/gov/nasa/worldwind/render/SurfaceObject.java new file mode 100644 index 0000000..8c296e2 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/render/SurfaceObject.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2014 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.avlist.AVList; +import gov.nasa.worldwind.geom.ExtentHolder; +import gov.nasa.worldwind.geom.Sector; + +import java.util.List; + +/** + * Common interface for renderables that are drawn on the Globe's surface terrain, such as {@link + * SurfaceShape}. SurfaceObject implements the {@link Renderable} + * and {@link gov.nasa.worldwind.render.PreRenderable} interfaces, so a surface object may be aggregated within any + * layer or within some arbitrary rendering code. + *

    + * SurfaceObjects automatically aggregate themselves in the DrawContext's ordered surface renderable queue by calling + * {@link DrawContext#addOrderedSurfaceRenderable(gov.nasa.worldwind.render.OrderedRenderable)} during the preRender, + * pick, and render stages. This enables SurfaceObjects to be processed in bulk, and reduces texture memory consumption + * by sharing rendering resources amongst multiple SurfaceObjects. + *

    + * Implementations of SurfaceObject require that {@link #preRender(gov.nasa.worldwind.render.DrawContext)} is called before {@link + * #render(gov.nasa.worldwind.render.DrawContext)} and {@link #pick(gov.nasa.worldwind.render.DrawContext, android.graphics.Point)}, and that preRender is called at the appropriate + * stage in the current rendering cycle. Calling preRender locks in the SurfaceObject's visual appearance for any + * subsequent calls to pick or render until the next call preRender. + * + * @author dcollins + * @version $Id: SurfaceObject.java 1171 2013-02-11 21:45:02Z dcollins $ + */ +public interface SurfaceObject extends OrderedRenderable, PreRenderable, ExtentHolder, AVList +{ + /** + * Indicates whether the surface object should be drawn during rendering. + * + * @return true if the object is to be drawn, otherwise false. + */ + boolean isVisible(); + + /** + * Specifies whether the surface object should be drawn during rendering. + * + * @param visible true if the object is to be drawn, otherwise false. + */ + void setVisible(boolean visible); + + /** + * Returns an object that uniquely identifies the surface object's state on the specified draw context. This object + * is guaranteed to be globally unique; an equality test with a state key from another always returns false. + * + * @param dc the draw context the state key relates to. + * + * @return an object representing surface object's current state. + */ + Object getStateKey(DrawContext dc); + + /** + * Returns zero to indicate that the surface object's distance from the eye is unknown. SurfaceObjects are processed + * on the DrawContext's ordered surface renderable queue. Ordered surface renderables do not utilize the + * renderable's distance from the eye to determine draw order. + * + * @return zero, to indicate that the object's distance from the eye is unknown. + */ + double getDistanceFromEye(); + + /** + * Returns the delegate owner of the surface object. If non-null, the returned object replaces the surface object as + * the pickable object returned during picking. If null, the surface object itself is the pickable object returned + * during picking. + * + * @return the object used as the pickable object returned during picking, or null to indicate the the surface + * object is returned during picking. + */ + Object getDelegateOwner(); + + /** + * Specifies the delegate owner of the surface object. If non-null, the delegate owner replaces the surface object + * as the pickable object returned during picking. If null, the surface object itself is the pickable object + * returned during picking. + * + * @param owner the object to use as the pickable object returned during picking, or null to return the surface + * object. + */ + void setDelegateOwner(Object owner); + + /** + * Returns a {@link java.util.List} of {@link gov.nasa.worldwind.geom.Sector} instances that bound the surface + * object on the specified DrawContext. + * + * @param dc the DrawContext the surface object is related to. + * + * @return the surface object's bounding Sectors. + */ + List getSectors(DrawContext dc); +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/render/SurfaceObjectTileBuilder.java b/WorldWindAndroid/src/gov/nasa/worldwind/render/SurfaceObjectTileBuilder.java new file mode 100644 index 0000000..4d27066 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/render/SurfaceObjectTileBuilder.java @@ -0,0 +1,1368 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.render; + +import android.graphics.Point; +import android.opengl.GLES20; +import gov.nasa.worldwind.WorldWindowImpl; +import gov.nasa.worldwind.avlist.AVKey; +import gov.nasa.worldwind.avlist.AVList; +import gov.nasa.worldwind.avlist.AVListImpl; +import gov.nasa.worldwind.cache.Cacheable; +import gov.nasa.worldwind.geom.*; +import gov.nasa.worldwind.util.*; + +import java.util.*; + +import static android.opengl.GLES20.*; + + +/** + * Builds a list of {@link SurfaceTile} instances who's content is defined by a specified set + * of {@link SurfaceObject} references. It's typically not necessary to use + * SurfaceObjectTileBuilder directly. World Wind's default {@link gov.nasa.worldwind.SceneController} automatically + * batches instances of {@link SurfaceObject} in a single SurfaceObjectTileBuilder. + * Applications that need to draw basic surface shapes should use or extend {@link + * SurfaceShape} instead of using SurfaceObjectTileBuilder directly. + *

    + * Surface tiles are built by calling {@link #buildTiles(gov.nasa.worldwind.render.DrawContext, Iterable)} (DrawContext, Iterable)} with an + * Iterable of SurfaceObjects. This assembles a set of surface tiles that meet the resolution requirements for the + * specified draw context, then draws the SurfaceObjects into those offscreen surface tiles by calling {@link + * SurfaceObject#render(gov.nasa.worldwind.render.DrawContext)}. This process may temporarily use the framebuffer to + * perform offscreen rendering, and therefore should be called during the preRender method of a World Wind {@link + * gov.nasa.worldwind.layers.Layer}. See {@link PreRenderable} for details. Once built, the + * surface tiles can be rendered by a {@link SurfaceTileRenderer}. + *

    + * By default, SurfaceObjectTileBuilder creates texture tiles with a width and height of 512 pixels, and with internal + * format GL_RGBA. These parameters are configurable by calling {@link + * #setTileDimension(Point)} or {@link #setTileTextureFormat(int)}. + *

    + * The most common usage pattern for SurfaceObjectTileBuilder is to build the surface tiles from a set of SurfaceObjects + * during the preRender phase, then draw those surface tiles during the render phase. For example, a {@link + * Renderable} can use SurfaceObjectTileBuilder to draw a set of SurfaceObjects as follows: + *

    + * + *

    + * class MyRenderable implements Renderable, PreRenderable
    + * {
    + *     protected SurfaceObjectTileBuilder tileBuilder = new SurfaceObjectTileBuilder();
    + *     protected ArrayList tiles = new ArrayList();
    + *
    + *     public void preRender(DrawContext dc)
    + *     {
    + *         List surfaceObjects = Arrays.asList(
    + *             new SurfaceCircle(LatLon.fromDegrees(0, 100), 10000),
    + *             new SurfaceSquare(LatLon.fromDegrees(0, 101), 10000));
    + *         this.tiles.addAll(this.tileBuilder.buildSurfaceTiles(dc, surfaceObjects));
    + *     }
    + *
    + *     public void render(DrawContext dc)
    + *     {
    + *         dc.getGeographicSurfaceTileRenderer().renderTiles(dc, this.tiles);
    + *     }
    + * }
    + * 
    + * + * + * @author dcollins + * @version $Id: SurfaceObjectTileBuilder.java 1824 2014-01-22 22:41:10Z dcollins $ + */ +public class SurfaceObjectTileBuilder +{ + /** The default surface tile texture dimension, in pixels. */ + protected static final int DEFAULT_TEXTURE_DIMENSION = 512; + /** The default OpenGL internal format used to create surface tile textures. */ + protected static final int DEFAULT_TEXTURE_INTERNAL_FORMAT = GL_RGBA; + /** The default OpenGL pixel format used to create surface tile textures. */ + protected static final int DEFAULT_TEXTURE_PIXEL_FORMAT = GL_RGBA; + /** + * The default split scale. The split scale 2.9 has been empirically determined to render sharp lines and edges with + * the SurfaceShapes such as SurfacePolyline and SurfacePolygon. + */ + protected static final double DEFAULT_SPLIT_SCALE = 2.9; + /** The default level zero tile delta used to construct a LevelSet. */ + protected static final LatLon DEFAULT_LEVEL_ZERO_TILE_DELTA = LatLon.fromDegrees(36, 36); + /** + * The default number of levels used to construct a LevelSet. Approximately 0.1 meters per pixel at the Earth's + * equator. + */ + protected static final int DEFAULT_NUM_LEVELS = 17; + /** The next unique ID. This property is shared by all instances of SurfaceObjectTileBuilder. */ + protected static long nextUniqueId = 1; + /** + * Map associating a tile texture dimension to its corresponding LevelSet. This map is a class property in order to + * share LevelSets across all instances of SurfaceObjectTileBuilder. + */ + protected static Map levelSetMap = new HashMap(); + /** + * Map associating a tile texture dimension to a tile cache name. This map is an instance property so that the cache + * names for each instance and each tile dimension are unique. + */ + protected Map tileCacheNameMap = new HashMap(); + + /** + * Indicates the desired tile texture width and height, in pixels. Initially set to + * DEFAULT_TEXTURE_DIMENSION. + */ + protected Point tileDimension = new Point(DEFAULT_TEXTURE_DIMENSION, DEFAULT_TEXTURE_DIMENSION); + /** + * Indicates the currently used tile texture width and height, in pixels. This is different than the caller + * specified tileDimension if this value does not fit in the viewport, or is not a power of two. + * Initially null. + */ + protected Point currentTileDimension; + /** The surface tile OpenGL texture format. 0 indicates the default format is used. */ + protected int tileTextureFormat; + /** Controls if surface tiles are rendered using a linear filter or a nearest-neighbor filter. */ + protected boolean useLinearFilter = true; + /** Controls if mip-maps are generated for surface tile textures. */ + protected boolean useMipmaps; + /** Controls the tile resolution as distance changes between the globe's surface and the eye point. */ + protected double splitScale = DEFAULT_SPLIT_SCALE; + /** List of currently assembled surface objects. Used during tile assembly and updating. */ + protected List currentSurfaceObjects = new ArrayList(); + /** List of currently assembled surface tiles. */ + protected List currentTiles = new ArrayList(); + /** Support class used to render to an offscreen surface tile. */ + protected OGLRenderToTextureSupport rttSupport = new OGLRenderToTextureSupport(); + + /** + * Constructs a new SurfaceObjectTileBuilder with a tile width and height of 512, with the default tile + * texture format, with linear filtering enabled, and with mip-mapping disabled. + */ + public SurfaceObjectTileBuilder() + { + } + + /** + * Constructs a new SurfaceObjectTileBuilder width the specified tile dimension, tile texture format, and flags + * specifying if linear filtering and mip-mapping are enabled. + * + * @param tileTextureDimension the surface tile texture dimension, in pixels. + * @param tileTextureFormat the surface tile OpenGL texture format, or 0 to use the default format. + * @param useLinearFilter true to use linear filtering while rendering surface tiles; false to use + * nearest-neighbor filtering. + * @param useMipmaps true to generate mip-maps for surface tile textures; false otherwise. + * + * @throws IllegalArgumentException if the tile dimension is null. + */ + public SurfaceObjectTileBuilder(Point tileTextureDimension, int tileTextureFormat, boolean useLinearFilter, + boolean useMipmaps) + { + if (tileTextureDimension == null) + { + String message = Logging.getMessage("nullValue.DimensionIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.setTileDimension(tileTextureDimension); + this.setTileTextureFormat(tileTextureFormat); + this.setUseLinearFilter(useLinearFilter); + this.setUseMipmaps(useMipmaps); + } + + /** + * Returns the surface tile dimension. + * + * @return the surface tile dimension, in pixels. + */ + public Point getTileDimension() + { + return this.tileDimension; + } + + /** + * Specifies the preferred surface tile texture dimension. If the dimension is larger than the viewport dimension, + * this uses a dimension with width and height set to the largest power of two that is less than or equal to the + * specified dimension and the viewport dimension. + * + * @param dimension the surface tile dimension, in pixels. + * + * @throws IllegalArgumentException if the dimension is null. + */ + public void setTileDimension(Point dimension) + { + if (dimension == null) + { + String message = Logging.getMessage("nullValue.DimensionIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.tileDimension = dimension; + } + + /** + * Returns the surface tile's OpenGL texture format, or 0 to indicate that the default format is used. + * + * @return the OpenGL texture format, or 0 if the default format is used. + * + * @see #setTileTextureFormat(int) + */ + public int getTileTextureFormat() + { + return tileTextureFormat; + } + + /** + * Specifies the surface tile's OpenGL texture format. A value of 0 indicates that the default format should be + * used. Otherwise, the texture format may be one of the following:
    • GL_ALPHA
    • GL_ALPHA4
    • + *
    • GL_ALPHA8
    • GL_ALPHA12
    • GL_ALPHA16
    • GL_COMPRESSED_ALPHA
    • + *
    • GL_COMPRESSED_LUMINANCE
    • GL_COMPRESSED_LUMINANCE_ALPHA
    • GL_COMPRESSED_INTENSITY
    • + *
    • GL_COMPRESSED_RGB
    • GL_COMPRESSED_RGBA
    • GL_DEPTH_COMPONENT
    • GL_DEPTH_COMPONENT16
    • + *
    • GL_DEPTH_COMPONENT24
    • GL_DEPTH_COMPONENT32
    • GL_LUMINANCE
    • GL_LUMINANCE4
    • + *
    • GL_LUMINANCE8
    • GL_LUMINANCE12
    • GL_LUMINANCE16
    • GL_LUMINANCE_ALPHA
    • + *
    • GL_LUMINANCE4_ALPHA4
    • GL_LUMINANCE6_ALPHA2
    • GL_LUMINANCE8_ALPHA8
    • + *
    • GL_LUMINANCE12_ALPHA4
    • GL_LUMINANCE12_ALPHA12
    • GL_LUMINANCE16_ALPHA16
    • + *
    • GL_INTENSITY
    • GL_INTENSITY4
    • GL_INTENSITY8
    • GL_INTENSITY12
    • + *
    • GL_INTENSITY16
    • GL_R3_G3_B2
    • GL_RGB
    • GL_RGB4
    • GL_RGB5
    • GL_RGB8
    • + *
    • GL_RGB10
    • GL_RGB12
    • GL_RGB16
    • GL_RGBA
    • GL_RGBA2
    • GL_RGBA4
    • + *
    • GL_RGB5_A1
    • GL_RGBA8
    • GL_RGB10_A2
    • GL_RGBA12
    • GL_RGBA16
    • + *
    • GL_SLUMINANCE
    • GL_SLUMINANCE8
    • GL_SLUMINANCE_ALPHA
    • GL_SLUMINANCE8_ALPHA8
    • + *
    • GL_SRGB
    • GL_SRGB8
    • GL_SRGB_ALPHA
    • GL_SRGB8_ALPHA8
    + *

    + * If the texture format is any of GL_RGB, GL_RGB8, GL_RGBA, or GL_RGBA8, the tile builder attempts to + * use OpenGL framebuffer objects to render shapes to the texture tiles. Otherwise, this renders shapes to the + * framebuffer and copies the framebuffer contents to the texture tiles. + * + * @param textureFormat the OpenGL texture format, or 0 to use the default format. + */ + public void setTileTextureFormat(int textureFormat) + { + this.tileTextureFormat = textureFormat; + } + + /** + * Returns if linear filtering is used when rendering surface tiles. + * + * @return true if linear filtering is used; false if nearest-neighbor filtering is used. + */ + public boolean isUseLinearFilter() + { + return useLinearFilter; + } + + /** + * Specifies if linear filtering should be used when rendering surface tiles. + * + * @param useLinearFilter true to use linear filtering; false to use nearest-neighbor filtering. + */ + public void setUseLinearFilter(boolean useLinearFilter) + { + this.useLinearFilter = useLinearFilter; + } + + /** + * Returns if mip-maps are generated for surface tile textures. + * + * @return true if mip-maps are generated; false otherwise. + */ + public boolean isUseMipmaps() + { + return this.useMipmaps; + } + + /** + * Specifies if mip-maps should be generated for surface tile textures. + * + * @param useMipmaps true to generate mip-maps; false otherwise. + */ + public void setUseMipmaps(boolean useMipmaps) + { + this.useMipmaps = useMipmaps; + } + + /** + * Sets the parameter controlling the tile resolution as distance changes between the globe's surface and the eye + * point. Higher resolution is displayed as the split scale increases from 1.0. Lower resolution is displayed as the + * split scale decreases from 1.0. The default value is 2.9. + * + * @param splitScale a value near 1.0 that controls the tile's surface texel resolution as the distance between the + * globe's surface and the eye point change. Increasing values select higher resolution, + * decreasing values select lower resolution. The default value is 2.9. + */ + public void setSplitScale(double splitScale) + { + this.splitScale = splitScale; + } + + /** + * Returns the split scale value controlling the tile's surface texel resolution relative to the distance between + * the globe's surface at the image position and the eye point. + * + * @return the current split scale. + * + * @see #setSplitScale(double) + */ + public double getSplitScale() + { + return this.splitScale; + } + + /** + * Assembles the surface tiles and draws any SurfaceObjects in the iterable into those offscreen tiles. The surface + * tiles are assembled to meet the necessary resolution of to the draw context's {@link gov.nasa.worldwind.View}. + * This may temporarily use the framebuffer to perform offscreen rendering, and therefore should be called during + * the preRender method of a World Wind {@link gov.nasa.worldwind.layers.Layer}. + *

    + * This returns an empty List if the specified iterable is null, is empty or contains no SurfaceObjects. + * + * @param dc the draw context to build tiles for. + * @param iterable the iterable to gather SurfaceObjects from. + * + * @return a List of SurfaceTiles containing a composite representation of the specified SurfaceObjects. + * + * @throws IllegalArgumentException if the draw context is null. + */ + public List buildTiles(DrawContext dc, Iterable iterable) + { + if (dc == null) + { + String message = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.currentSurfaceObjects.clear(); + this.currentTiles.clear(); + + if (iterable == null) + return Collections.emptyList(); + + // Assemble the list of current surface objects from the specified iterable. + this.assembleSurfaceObjects(iterable); + + // We've cleared any tile assembly state from the last rendering pass. Determine if we can assemble and update + // the tiles. If not, we're done. + if (this.currentSurfaceObjects.isEmpty() || !this.canAssembleTiles(dc)) + return Collections.emptyList(); + + // Assemble the current visible tiles and update their associated textures if necessary. + this.assembleTiles(dc); + this.updateTiles(dc); + + // Tiles in the current tile list contain references to SurfaceObjects. These references are used during tile + // update, and are longer needed. Clear these lists to ensure we don't retain any dangling references to the + // surface objects. + for (SurfaceObjectTile tile : this.currentTiles) + { + tile.clearObjectList(); + } + + ArrayList tiles = new ArrayList(this.currentTiles); + + // Objects in the current surface object list and current tile list are no longer needed. Clear the lists to + // ensure we don't retain dangling references to any objects or tiles. + this.currentSurfaceObjects.clear(); + this.currentTiles.clear(); + + if(WorldWindowImpl.DEBUG) + Logging.verbose("Built surface object tiles #" + tiles.size()); + return tiles; + } + + /** + * Indicates the tile cache name to use for the specified tileDimension. The cache name is unique to + * this SurfaceObjectTileBuilder and the tileDimension. Using a unique cache name for each + * instance and tile dimension ensures that the tiles for this instances and tileDimension do not + * conflict with any other tiles in the cache. + *

    + * In practices, there are at most 10 dimensions we'll use (512, 256, 128, 64, 32, 16, 8, 4, 2, 1 ), and therefore + * at most 10 cache names for each SurfaceObjectTileBuilder. + *

    + * Subsequent calls are guaranteed to return the same cache name for the same tileDimension. + * + * @param tileDimension the texture tile dimension who's cache name is returned. + * + * @return the tile cache name for the specified tileDimension + */ + protected String getTileCacheName(Point tileDimension) + { + String s = this.tileCacheNameMap.get(tileDimension); + if (s == null) + { + s = this.nextCacheName(); + this.tileCacheNameMap.put(tileDimension, s); + } + + return s; + } + + /** + * Returns a unique name appropriate for use as part of a cache name. The returned string is constructed as follows: + * {@code this.getClass().getName() + "/" + nextUniqueId()}. + * + * @return a unique cache name. + */ + protected String nextCacheName() + { + StringBuilder sb = new StringBuilder(); + sb.append(this.getClass().getName()); + sb.append("/"); + sb.append(nextUniqueId()); + + return sb.toString(); + } + + /** + * Returns the next unique integer associated with a SurfaceObjectTileBuilder. This method is synchronized to ensure + * that two threads calling simultaneously receive different IDs. Since this method is called from + * SurfaceObjectTileBuilder's constructor, this is critical to ensure that SurfaceObjectTileBuilders can be safely + * constructed on separate threads. + * + * @return the next unique integer. + */ + protected static synchronized long nextUniqueId() + { + return nextUniqueId++; + } + + /** + * Returns the tile dimension used to create the tile textures for the specified DrawContext. This + * attempts to use the caller configured {@link #tileDimension}, but always returns a dimension that is + * is a power of two, is square, and fits in the DrawContext's viewport. + * + * @param dc the DrawContext to compute a texture tile dimension for. + * + * @return a texture tile dimension appropriate for the specified DrawContext. + */ + protected Point computeTextureTileDimension(DrawContext dc) + { + // Force a square dimension by using the maximum of the tile builder's tileWidth and tileHeight. + int maxSize = Math.max(this.tileDimension.x, this.tileDimension.y); + + // The viewport may be smaller than the desired dimension. For that reason, we constrain the desired tile + // dimension by the viewport width and height. + Rect viewport = dc.getView().getViewport(); + if (maxSize > viewport.width) + maxSize = (int)viewport.width; + if (maxSize > viewport.height) + maxSize = (int)viewport.height; + + // The final dimension used to render all surface tiles will be the power of two which is less than or equal to + // the preferred dimension, and which fits into the viewport. + + int potSize = WWMath.powerOfTwoFloor(maxSize); + return new Point(potSize, potSize); + } + + //**************************************************************// + //******************** Tile Updating *************************// + //**************************************************************// + + /** + * Updates each {@link gov.nasa.worldwind.render.SurfaceObjectTileBuilder.SurfaceObjectTile} in the {@link #currentTiles} list. This is + * typically called after {@link #assembleTiles(gov.nasa.worldwind.render.DrawContext)} to update the assembled tiles. + *

    + * This method does nothing if currentTiles is empty. + * + * @param dc the draw context the tiles relate to. + */ + protected void updateTiles(DrawContext dc) + { + if (this.currentTiles.isEmpty()) + return; + + Logging.info("Updating tiles #" + currentTiles.size()); + + // The tile drawing rectangle has the same dimension as the current tile viewport, but it's lower left corner + // is placed at the origin. This is because the orthographic projection setup by OGLRenderToTextureSupport + // maps (0, 0) to the lower left corner of the drawing region, therefore we can drop the (x, y) offset when + // drawing pixels to the texture, as (0, 0) is automatically mapped to (x, y). Since we've created the tiles + // from a LevelSet where each level has equivalent dimension, we assume that tiles in the current tile list + // have equivalent dimension. + + // The OpenGL framebuffer object extension used by RenderToTextureSupport works only for texture formats + // GL_RGB and GL_RGBA. Disable framebuffer objects if the tile builder has been configured with a different + // format. + this.rttSupport.setEnableFramebufferObject( + this.tileTextureFormat == 0 || // Default format is GL_RGB8. + this.tileTextureFormat == GL_RGB || + this.tileTextureFormat == GL_RGBA); + + Matrix projection = this.rttSupport.beginRendering(dc, 0, 0, this.currentTileDimension.x, this.currentTileDimension.y); + try + { + for (SurfaceObjectTile tile : this.currentTiles) + { + this.updateTile(dc, tile, projection); + } + } + finally + { + this.rttSupport.endRendering(dc); + } + } + + /** + * Draws the current list of SurfaceObjects into the specified surface tile. The surface tiles is updated only when + * necessary. The tile keeps track of the list of SurfaceObjects rendered into it, and the state keys those objects. + * The tile is updated if the list changes, if any of the state keys change, or if the tile has no texture. + * Otherwise the tile is left unchanged and the update is skipped. + * + * @param dc the draw context the tile relates to. + * @param tile the tile to update. + */ + protected void updateTile(DrawContext dc, SurfaceObjectTile tile, Matrix projection) + { + // Get the tile's texture from the draw context's texture cache. If null we create a new texture and update the + // texture cache below. + GpuTexture texture = tile.getTexture(dc.getTextureCache()); + + // Compare the previous tile state against the currently computed state to determine if the tile needs to be + // updated. The tile needs to be updated if any the following conditions are true: + // * The tile has no texture. + // * The tile has no state. + // * The list of intersecting objects has changed. + // * An intersecting object's state key is different than one stored in the tile's previous state key. + Object tileStateKey = tile.getStateKey(dc); + + if (texture != null && tileStateKey.equals(tile.lastUpdateStateKey)) + return; + + // If the tile needs to be updated, then assign its lastUpdateStateKey before its texture is created. This + // ensures that the lastUpdateStateKey is current when the tile is added to the cache. + tile.lastUpdateStateKey = tileStateKey; + + if (texture == null) // Create the tile's texture if it doesn't already have one. + { + Logging.verbose("Creating tile texture"); + texture = this.createTileTexture(dc, tile.getWidth(), tile.getHeight()); + tile.setTexture(dc.getTextureCache(), texture); + } + + if (texture == null) // This should never happen, but we check anyway. + { + Logging.warning(Logging.getMessage("nullValue.TextureIsNull")); + return; + } + if(WorldWindowImpl.DEBUG) + Logging.verbose("Rendering tile texture"); + try + { + // SurfaceObjects expect the SurfaceTileDrawContext to be attached to the draw context's AVList. Create a + // SurfaceTileDrawContext with the tile's Sector and viewport. The Sector defines the context's geographic + // extent, and the viewport defines the context's corresponding viewport in pixels. + dc.setValue(AVKey.SURFACE_TILE_DRAW_CONTEXT, this.createSurfaceTileDrawContext(tile, projection)); + + this.rttSupport.setColorTarget(dc, texture); + this.rttSupport.clear(dc, new Color(0, 0, 0, 0)); // Set all texture pixels to transparent black. + + if (tile.hasObjects()) + { + for (SurfaceObject so : tile.getObjectList()) + { + dc.setCurrentLayer(so.getLayer()); + so.render(dc); + dc.setCurrentLayer(null); + } + } + } + finally + { + this.rttSupport.setColorTarget(dc, null); + + dc.removeKey(AVKey.SURFACE_TILE_DRAW_CONTEXT); + } + } + + /** + * Returns a new surface tile texture for use on the specified draw context with the specified width and height. + *

    + * The returned texture's internal format is specified by tilePixelFormat. If + * tilePixelFormat is zero, this returns a texture with internal format GL_RGBA8. + *

    + * The returned texture's parameters are configured as follows: + * + * + * @param dc the draw context to create a texture for. + * @param width the texture's width, in pixels. + * @param height the texture's height, in pixels. + * + * @return a new texture with the specified width and height. + */ + protected GpuTexture createTileTexture(DrawContext dc, int width, int height) + { + Logging.verbose(String.format("createTileTexture(%d, %d)", width, height)); + int internalFormat = this.tileTextureFormat == 0 ? DEFAULT_TEXTURE_INTERNAL_FORMAT : tileTextureFormat; + + int [] textureId = new int[1]; + glGenTextures(1, textureId, 0); + + glBindTexture(GL_TEXTURE_2D, textureId[0]); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, this.isUseLinearFilter() ? + (this.isUseMipmaps() ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR) : GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, this.isUseLinearFilter() ? + GL_LINEAR : GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, null); + + if(isUseMipmaps()) { + glGenerateMipmap(textureId[0]); + } + + glBindTexture(GL_TEXTURE_2D, 0); + + GpuTexture t = new GpuTexture(GL_TEXTURE_2D, textureId, width, height, + OGLUtil.estimateMemorySize(internalFormat, GL_UNSIGNED_BYTE, width, height, isUseMipmaps()), Matrix.fromIdentity()); + + return t; + } + + /** + * Returns a new Object representing the drawing context for the specified tile. The returned object should + * represent the tile's sector and it's corresponding viewport in pixels. + * + * @param tile The tile to create a context for. + * + * @return a new drawing context for the specified tile. + */ + protected Object createSurfaceTileDrawContext(SurfaceObjectTile tile, Matrix projection) + { + return new SurfaceTileDrawContext(tile.getSector(), tile.getWidth(), tile.getHeight(), projection); + } + + //**************************************************************// + //******************** Surface Object Assembly ***************// + //**************************************************************// + + /** + * Adds any SurfaceObjects in the specified Iterable to the tile builder's {@link #currentSurfaceObjects} list. + * + * @param iterable the Iterable to gather SurfaceObjects from. + */ + protected void assembleSurfaceObjects(Iterable iterable) + { + // Gather up all the SurfaceObjects, ignoring null references and non SurfaceObjects. + for (Object o : iterable) + { + if (o instanceof SurfaceObject) + this.currentSurfaceObjects.add((SurfaceObject) o); + } + } + + //**************************************************************// + //******************** LevelSet Assembly *********************// + //**************************************************************// + + /** + * Returns a shared LevelSet for the specified tileDimension. All instances of + * SurfaceObjectTileBuilder share common LevelSets to determine which tiles are visible, but create + * unique tile instances and uses a unique tile cache name. Since all instances use the same tile structure to + * determine visible tiles, this saves memory while ensuring that each instance stores its own tiles in the cache. + *

    + * The returned LevelSet's cache name and dataset name are dummy values, and should not be used. Use this tile + * builder's cache name for the specified tileDimension instead. + *

    + * In practice, there are at most 10 dimensions we use: 512, 256, 128, 64, 32, 16, 8, 4, 2, 1. Therefore keeping the + * LevelSets in a map requires little memory overhead, and ensures each LevelSet is + * retained once constructed. Retaining references to the LevelSets means we're able to re-use the + * texture resources associated with each LevelSet in the DrawContext's texture cache. + *

    + * Subsequent calls are guaranteed to return the same LevelSet for the same + * tileDimension. + * + * @param tileDimension the DrawContext to return a LevelSet for. + * + * @return a LevelSet who's tile dimension fits in the DrawContext's viewport. + */ + protected LevelSet getLevelSet(Point tileDimension) + { + // If we already have a LevelSet for the dimension, just return it. Otherwise create it and put it in a map for + // use during subsequent calls. + LevelSet levelSet = levelSetMap.get(tileDimension); + if (levelSet == null) + { + levelSet = createLevelSet(tileDimension.x, tileDimension.y); + levelSetMap.put(tileDimension, levelSet); + } + + return levelSet; + } + + /** + * Returns a new LevelSet with the specified tile width and height. The LevelSet overs the full sphere, has a level + * zero tile delta of {@link #DEFAULT_LEVEL_ZERO_TILE_DELTA}, has number of levels equal to {@link + * #DEFAULT_NUM_LEVELS} (with no empty levels). The LevelSets' cache name and dataset name dummy values, and should + * not be used. + * + * @param tileWidth the LevelSet's tile width, in pixels. + * @param tileHeight the LevelSet's tile height, in pixels. + * + * @return a new LevelSet configured to with + */ + protected static LevelSet createLevelSet(int tileWidth, int tileHeight) + { + AVList params = new AVListImpl(); + params.setValue(AVKey.LEVEL_ZERO_TILE_DELTA, DEFAULT_LEVEL_ZERO_TILE_DELTA); + params.setValue(AVKey.SECTOR, Sector.FULL_SPHERE); + params.setValue(AVKey.NUM_LEVELS, DEFAULT_NUM_LEVELS); + params.setValue(AVKey.NUM_EMPTY_LEVELS, 0); + params.setValue(AVKey.TILE_WIDTH, tileWidth); + params.setValue(AVKey.TILE_HEIGHT, tileHeight); + // This is a shared LevelSet, so just supply a dummy cache name and dataset name. + params.setValue(AVKey.DATA_CACHE_NAME, SurfaceObjectTileBuilder.class.getName()); + params.setValue(AVKey.DATASET_NAME, SurfaceObjectTileBuilder.class.getName()); + // We won't use any tile resource paths, so just supply a dummy format suffix. + params.setValue(AVKey.FORMAT_SUFFIX, SurfaceObjectTileBuilder.class.getName()); + + return new LevelSet(params); + } + + //**************************************************************// + //******************** Tile Assembly *************************// + //**************************************************************// + + /** + * Returns true if the draw context's viewport width and height are greater than zero. + * + * @param dc the DrawContext to test. + * + * @return true if the DrawContext's has a non-zero viewport; false otherwise. + */ + protected boolean canAssembleTiles(DrawContext dc) + { + Rect viewport = dc.getView().getViewport(); + return viewport.width > 0 && viewport.height > 0; + } + + /** + * Assembles a set of surface tiles that are visible in the specified DrawContext and meet the tile builder's + * resolution criteria. Tiles are culled against the current SurfaceObject list, against the DrawContext's view + * frustum during rendering mode, and against the DrawContext's pick frustums during picking mode. If a tile does + * not meet the tile builder's resolution criteria, it's split into four sub-tiles and the process recursively + * repeated on the sub-tiles. Visible leaf tiles are added to the {@link #currentTiles} list. + *

    + * During assembly each SurfaceObject in {@link #currentSurfaceObjects} is sorted into the tiles they intersect. The + * top level tiles are used as an index to quickly determine which tiles each SurfaceObjects intersects. + * SurfaceObjects are sorted into sub-tiles by simple intersection tests. SurfaceObjects are added to each tile's + * surface object list at most once. See {@link gov.nasa.worldwind.render.SurfaceObjectTileBuilder.SurfaceObjectTile#addSurfaceObject(gov.nasa.worldwind.render.SurfaceObject, + * gov.nasa.worldwind.geom.Sector)}. Tiles that don't intersect any SurfaceObjects are discarded. + * + * @param dc the DrawContext to assemble tiles for. + */ + protected void assembleTiles(DrawContext dc) + { + Logging.info("Assembling tiles object #" + currentSurfaceObjects.size()); + // Compute the texture tile dimension to use for this DrawContext. This dimension is always square and a power + // of two. + this.currentTileDimension = this.computeTextureTileDimension(dc); + // Get the level set and tile cache name to use for the current tile dimension. The LevelSet is shared by all + // SurfaceObjectTileBuilders, while the cache name is unique to this instance. Since all + // SurfaceObjectTileBuilders use the same tile structure to determine visible tiles, this saves memory while + // preventing a conflict in the tile and texture caches. + String tileCacheName = this.getTileCacheName(this.currentTileDimension); + LevelSet levelSet = this.getLevelSet(this.currentTileDimension); + + Level level = levelSet.getFirstLevel(); + Angle dLat = level.getTileDelta().getLatitude(); + Angle dLon = level.getTileDelta().getLongitude(); + Angle latOrigin = levelSet.getTileOrigin().getLatitude(); + Angle lonOrigin = levelSet.getTileOrigin().getLongitude(); + + // Store the top level tiles in a set to ensure that each top level tile is added only once. Store the tiles + // that intersect each surface object in a set to ensure that each object is added to a tile at most once. + Set topLevelTiles = new HashSet(); + Set intersectingTileKeys = new HashSet(); + + // Iterate over the current surface objects, adding each surface object to the top level tiles that it + // intersects. This produces a set of top level tiles containing the surface objects that intersect each tile. + // We use the tile structure as an index to quickly determine the tiles a surface object intersects, and add + // object to those tiles. This has the effect of quickly sorting the objects into the top level tiles. We + // collect the top level tiles in a HashSet to ensure there are no duplicates when multiple objects intersect + // the same top level tiles. + for (SurfaceObject so : this.currentSurfaceObjects) + { + List sectors = so.getSectors(dc); + if (sectors == null) + continue; + + if(WorldWindowImpl.DEBUG) + Logging.verbose(String.format("Surface object sectors: %s", sectors)); + for (Sector s : sectors) + { + // Use the LevelSets tiling scheme to index the surface object's sector into the top level tiles. This + // index operation is faster than computing an intersection test between each tile and the list of + // surface objects. + int firstRow = Tile.computeRow(dLat, s.minLatitude, latOrigin); + int firstCol = Tile.computeColumn(dLon, s.minLongitude, lonOrigin); + int lastRow = Tile.computeRow(dLat, s.maxLatitude, latOrigin); + int lastCol = Tile.computeColumn(dLon, s.maxLongitude, lonOrigin); + + Angle p1 = Tile.computeRowLatitude(firstRow, dLat, latOrigin); + for (int row = firstRow; row <= lastRow; row++) + { + Angle p2; + p2 = p1.add(dLat); + + Angle t1 = Tile.computeColumnLongitude(firstCol, dLon, lonOrigin); + for (int col = firstCol; col <= lastCol; col++) + { + Angle t2; + t2 = t1.add(dLon); + + Object tileKey = this.createTileKey(level, row, col, tileCacheName); + + // Ignore this tile if the surface object has already been added to it. This handles dateline + // spanning surface objects which have two sectors that share a common boundary. + if (intersectingTileKeys.contains(tileKey)) + continue; + + SurfaceObjectTile tile = (SurfaceObjectTile) GpuTextureTile.getMemoryCache().get(tileKey); + if (tile == null) + { + tile = this.createTile(new Sector(p1, p2, t1, t2), level, row, col, tileCacheName); + GpuTextureTile.getMemoryCache().put(tileKey, tile); + } + + intersectingTileKeys.add(tileKey); // Set of intersecting tile keys ensure no duplicate objects. + topLevelTiles.add(tile); // Set of top level tiles ensures no duplicates tiles. + tile.addSurfaceObject(so, s); + + t1 = t2; + } + p1 = p2; + } + } + + intersectingTileKeys.clear(); // Clear the intersecting tile keys for the next surface object. + } + + if(WorldWindowImpl.DEBUG) + Logging.verbose(String.format("Created %d top level surface shape tiles", topLevelTiles.size())); + + // Add each top level tile or its descendants to the current tile list. + for (SurfaceObjectTile tile : topLevelTiles) + { + this.addTileOrDescendants(dc, levelSet, null, tile); + } + } + + /** + * Potentially adds the specified tile or its descendants to the tile builder's {@link #currentTiles} list. The tile + * and its descendants are discarded if the tile is not visible or does not intersect any SurfaceObjects in the + * parent's surface object list. See {@link gov.nasa.worldwind.render.SurfaceObjectTileBuilder.SurfaceObjectTile#getObjectList()}. + *

    + * If the tile meet the tile builder's resolution criteria it's added to the tile builder's + * currentTiles list. Otherwise, it's split into four sub-tiles and each tile is recursively processed. + * See {@link #meetsRenderCriteria(gov.nasa.worldwind.render.DrawContext, gov.nasa.worldwind.util.LevelSet, gov.nasa.worldwind.util.Tile)}. + * + * @param dc the current DrawContext. + * @param levelSet the tile's LevelSet. + * @param parent the tile's parent, or null if the tile is a top level tile. + * @param tile the tile to add. + */ + protected void addTileOrDescendants(DrawContext dc, LevelSet levelSet, SurfaceObjectTile parent, + SurfaceObjectTile tile) + { +// if(WorldWindowImpl.DEBUG) Logging.verbose(String.format("Checking tile %s @%s", tile, tile.getSector())); + + // Ignore this tile if it falls completely outside the DrawContext's visible sector. + if (!this.intersectsVisibleSector(dc, tile)) + { +// if(WorldWindowImpl.DEBUG) Logging.verbose(String.format("!intersect visible sector throwing away")); + // This tile is not added to the current tile list, so we clear it's object list to prepare it for use + // during the next frame. + tile.clearObjectList(); + return; + } + + // Ignore this tile if it falls completely outside the DrawContext's visible sector. +// if (!this.intersectsFrustum(dc, tile)) +// { +// if(WorldWindowImpl.DEBUG) Logging.verbose(String.format("!Intersect frustrum throwing away")); +// // This tile is not added to the current tile list, so we clear it's object list to prepare it for use +// // during the next frame. +// tile.clearObjectList(); +// return; +// } + +// if(WorldWindowImpl.DEBUG) Logging.verbose(String.format("Visible checking for objects")); + + // If the parent tile is not null, add any parent surface objects that intersect this tile. + if (parent != null) + this.addIntersectingObjects(dc, parent, tile); + + // Ignore tiles that do not intersect any surface objects. + if (!tile.hasObjects()) { +// if(WorldWindowImpl.DEBUG) Logging.verbose("!tile.hasObjects()"); + return; + } +// if(WorldWindowImpl.DEBUG) Logging.verbose(String.format("Tile has objects: %s", tile.getObjectList())); + + // If this tile meets the current rendering criteria, add it to the current tile list. This tile's object list + // is cleared after the tile update operation. + if (this.meetsRenderCriteria(dc, levelSet, tile)) + { + if(WorldWindowImpl.DEBUG) Logging.verbose(String.format("Meets render criteria so adding this tile")); + + this.addTile(tile); + return; + } + + Level nextLevel = levelSet.getLevel(tile.getLevelNumber() + 1); +// if(WorldWindowImpl.DEBUG) Logging.verbose(String.format("Splitting tile to next level: %d", nextLevel.getLevelNumber())); + for (GpuTextureTile subTile : tile.createSubTiles(nextLevel)) + { + this.addTileOrDescendants(dc, levelSet, tile, (SurfaceObjectTile) subTile); + } + + // This tile is not added to the current tile list, so we clear it's object list to prepare it for use during + // the next frame. + tile.clearObjectList(); + } + + /** + * Adds SurfaceObjects from the parent's object list to the specified tile's object list. If the tile's sector does + * not intersect the sector bounding the parent's object list, this does nothing. Otherwise, this adds any of the + * parent's SurfaceObjects that intersect the tile's sector to the tile's object list. + * + * @param dc the current DrawContext. + * @param parent the tile's parent. + * @param tile the tile to add intersecting SurfaceObject to. + */ + protected void addIntersectingObjects(DrawContext dc, SurfaceObjectTile parent, SurfaceObjectTile tile) + { + // If the parent has no objects, then there's nothing to add to this tile and we exit immediately. + if (!parent.hasObjects()) + return; + + // If this tile does not intersect the parent's object bounding sector, then none of the parent's objects + // intersect this tile. Therefore we exit immediately, and do not add any objects to this tile. + if (!tile.getSector().intersects(parent.getObjectSector())) + return; + + // If this tile contains the parent's object bounding sector, then all of the parent's objects intersect this + // tile. Therefore we just add all of the parent's objects to this tile. Additionally, the parent's object + // bounding sector becomes this tile's object bounding sector. + if (tile.getSector().contains(parent.getObjectSector())) + { + tile.addAllSurfaceObjects(parent.getObjectList(), parent.getObjectSector()); + } + // Otherwise, the tile may intersect some of the parent's object list. Compute which objects intersect this + // tile, and compute this tile's bounding sector as the union of those object's sectors. + else + { + for (SurfaceObject so : parent.getObjectList()) + { + List sectors = so.getSectors(dc); + if (sectors == null) + continue; + + // Test intersection against each of the SurfaceObject's sectors. We break after finding an intersection + // to avoid adding the same object to the tile more than once. + for (Sector s : sectors) + { + if (tile.getSector().intersects(s)) + { + tile.addSurfaceObject(so, s); + break; + } + } + } + } + } + + /** + * Adds the specified tile to the tile builder's {@link #currentTiles} list. + * + * @param tile the tile to add. + */ + protected void addTile(SurfaceObjectTile tile) + { + this.currentTiles.add(tile); + GpuTextureTile.getMemoryCache().put(tile.getTileKey(), tile); + } + + /** + * Test if the tile intersects the specified draw context's frustum. During picking mode, this tests intersection + * against all of the draw context's pick frustums. During rendering mode, this tests intersection against the draw + * context's viewing frustum. + * + * @param dc the draw context the SurfaceObject is related to. + * @param tile the tile to test for intersection. + * + * @return true if the tile intersects the draw context's frustum; false otherwise. + */ + protected boolean intersectsFrustum(DrawContext dc, GpuTextureTile tile) + { + Extent extent = tile.getExtent(); + if (extent == null) + return false; + + if (dc.isPickingMode()) + return dc.getPickFrustums().intersectsAny(extent); + + return dc.getView().getFrustumInModelCoordinates().intersects(extent); + } + + /** + * Test if the specified tile intersects the draw context's visible sector. This returns false if the draw context's + * visible sector is null. + * + * @param dc the current draw context. + * @param tile the tile to test for intersection. + * + * @return true if the tile intersects the draw context's visible sector; false otherwise. + */ + protected boolean intersectsVisibleSector(DrawContext dc, GpuTextureTile tile) + { + return dc.getVisibleSector() != null && dc.getVisibleSector().intersects(tile.getSector()); + } + + /** + * Tests if the specified tile meets the rendering criteria on the specified draw context. This returns true if the + * tile is from the level set's final level, or if the tile achieves the desired resolution on the draw context. + * + * @param dc the current draw context. + * @param levelSet the level set the tile belongs to. + * @param tile the tile to test. + * + * @return true if the tile meets the rendering criteria; false otherwise. + */ + protected boolean meetsRenderCriteria(DrawContext dc, LevelSet levelSet, Tile tile) + { + return levelSet.isFinalLevel(tile.getLevel().getLevelNumber()) || !this.needToSplit(dc, tile); + } + + /** + * Tests if the specified tile must be split to meets the desired resolution on the specified draw context. This + * compares the distance form the eye point to the tile to determine if the tile meets the desired resolution for + * the {@link gov.nasa.worldwind.View} attached to the draw context. + * + * @param dc the current draw context. + * @param tile the tile to test. + * + * @return true if the tile must be split; false otherwise. + */ + protected boolean needToSplit(DrawContext dc, Tile tile) + { + // Compute the height in meters of a texel from the specified tile. Take care to convert from the radians to + // meters by multiplying by the globe's radius, not the length of a Cartesian point. Using the length of a + // Cartesian point is incorrect when the globe is flat. + double texelSizeRadians = tile.getLevel().getTexelSize(); + double texelSizeMeters = dc.getGlobe().getRadius() * texelSizeRadians; + + // Compute the level of detail scale and the field of view scale. These scales are multiplied by the eye + // distance to derive a scaled distance that is then compared to the texel size. The level of detail scale is + // specified as a power of 10. For example, a detail factor of 3 means split when the cell size becomes more + // than one thousandth of the eye distance. The field of view scale is specified as a ratio between the current + // field of view and a the default field of view. In a perspective projection, decreasing the field of view by + // 50% has the same effect on object size as decreasing the distance between the eye and the object by 50%. + double detailScale = Math.pow(10, -this.getSplitScale()); + double fieldOfViewScale = dc.getView().getFieldOfView().tanHalfAngle() / Angle.fromDegrees(45).tanHalfAngle(); + fieldOfViewScale = WWMath.clamp(fieldOfViewScale, 0, 1); + + // Compute the distance between the eye point and the sector in meters, and compute a fraction of that distance + // by multiplying the actual distance by the level of detail scale and the field of view scale. + double eyeDistanceMeters = tile.getSector().distanceTo(dc, dc.getView().getEyePoint()); + double scaledEyeDistanceMeters = eyeDistanceMeters * detailScale * fieldOfViewScale; + + // Split when the texel size in meters becomes greater than the specified fraction of the eye distance, also in + // meters. Another way to say it is, use the current tile if its texel size is less than the specified fraction + // of the eye distance. + // + // NOTE: It's tempting to instead compare a screen pixel size to the texel size, but that calculation is + // window-size dependent and results in selecting an excessive number of tiles when the window is large. + return texelSizeMeters > scaledEyeDistanceMeters; + } + + //**************************************************************// + //******************** Surface Object Tile *******************// + //**************************************************************// + + /** + * Returns a new SurfaceObjectTile corresponding to the specified {@code sector}, {@code level}, {@code row}, {@code + * column}, and {@code cacheName}. + * + * @param sector The tile's Sector. + * @param level The tile's Level in a {@link gov.nasa.worldwind.util.LevelSet}. + * @param row The tile's row in the Level, starting from 0 and increasing to the right. + * @param column The tile's column in the Level, starting from 0 and increasing upward. + * @param cacheName Tile tile's cache name. + * + * @return a new SurfaceObjectTile. + */ + protected SurfaceObjectTile createTile(Sector sector, Level level, int row, int column, String cacheName) + { + return new SurfaceObjectTile(sector, level, row, column, cacheName); + } + + /** + * Returns a new tile key corresponding to the tile with the specified {@code level}, {@code row}, {@code column}, + * and {@code cacheName}. + * + * @param level The tile's Level in a {@link gov.nasa.worldwind.util.LevelSet}. + * @param row The tile's row in the Level, starting from 0 and increasing to the right. + * @param column The tile's column in the Level, starting from 0 and increasing upward. + * @param cacheName Tile tile's cache name. + * + * @return a tile key. + */ + protected Object createTileKey(Level level, int row, int column, String cacheName) + { + return new TileKey(level.getLevelNumber(), row, column, cacheName); + } + + /** + * Represents a {@link gov.nasa.worldwind.render.GpuTextureTile} who's contents is constructed by a set of surface + * objects. The tile maintains a collection of surface objects that intersect the tile, and provides methods for to + * modify and retrieve that collection. Additionally, the method {@link #getStateKey(gov.nasa.worldwind.render.DrawContext)} provides a + * mechanism to uniquely identify the tile's current state, including the state of each intersecting surface + * object. + */ + protected static class SurfaceObjectTile extends GpuTextureTile + { + /** The sector that bounds the surface objects intersecting the tile. */ + protected Sector objectSector; + /** List of surface objects intersecting the tile. */ + protected List intersectingObjects; + /** The state key that was valid when the tile was last updated. */ + protected Object lastUpdateStateKey; + + /** + * Constructs a tile for a given sector, level, row and column of the tile's containing tile set. + * + * @param sector The sector corresponding with the tile. + * @param level The tile's level within a containing level set. + * @param row The row index (0 origin) of the tile within the indicated level. + * @param column The column index (0 origin) of the tile within the indicated level. + * @param cacheName The tile's cache name. Overrides the Level's cache name to associates the tile with it's + * tile builder in a global cache. + * + * @throws IllegalArgumentException if any of the {@code sector}, {@code level}, or {@code cacheName } are + * {@code null}. + */ + public SurfaceObjectTile(Sector sector, Level level, int row, int column, String cacheName) + { + super(sector, level, row, column, cacheName); + } + + /** + * Returns the tile's size in bytes. Overridden to append the size of the {@link #cacheName} and the {@link + * #lastUpdateStateKey} to the superclass' computed size. + * + * @return The tile's size in bytes. + */ + @Override + public long getSizeInBytes() + { + long size = super.getSizeInBytes(); + + if (this.lastUpdateStateKey instanceof Cacheable) + size += ((Cacheable) this.lastUpdateStateKey).getSizeInBytes(); + else if (this.lastUpdateStateKey != null) + size += 4; // If the object doesn't implement Cacheable, just account for the reference to it. + + return size; + } + + /** + * Returns an object that uniquely identifies the tile's state on the specified draw context. This object is + * guaranteed to be globally unique; an equality test with a state key from another always returns false. + * + * @param dc the draw context the state key relates to. + * + * @return an object representing surface object's current state. + */ + public Object getStateKey(DrawContext dc) + { + return new SurfaceObjectTileStateKey(dc, this); + } + + /** + * Returns a sector that bounds the surface objects intersecting the tile. This returns null if no surface + * objects intersect the tile. + * + * @return a sector bounding the tile's intersecting objects. + */ + public Sector getObjectSector() + { + return this.objectSector; + } + + /** + * Returns whether list of surface objects intersecting this tile has elements. + * + * @return {@code true} if the list of surface objects intersecting this tile has elements, and {@code false} + * otherwise. + */ + public boolean hasObjects() + { + return this.intersectingObjects != null && !this.intersectingObjects.isEmpty(); + } + + /** + * Returns a list of surface objects intersecting the tile. + * + * @return a tile's intersecting objects. + */ + public List getObjectList() + { + return this.intersectingObjects; + } + + /** + * Clears the tile's list of intersecting objects. {@link #getObjectSector()} returns null after calling this + * method. + */ + public void clearObjectList() + { + this.intersectingObjects = null; + this.objectSector = null; + } + + /** + * Adds the specified surface object to the tile's list of intersecting objects. + * + * @param so the surface object to add. + * @param sector the sector bounding the specified surface object. + */ + public void addSurfaceObject(SurfaceObject so, Sector sector) + { + if (this.intersectingObjects == null) + this.intersectingObjects = new ArrayList(); + + this.intersectingObjects.add(so); + this.objectSector = (this.objectSector != null) ? this.objectSector.union(sector) : sector; + } + + /** + * Adds the specified collection of surface objects to the tile's list of intersecting objects. + * + * @param c the collection of surface objects to add. + * @param sector the sector bounding the specified surface object collection. + */ + public void addAllSurfaceObjects(List c, Sector sector) + { + if (this.intersectingObjects == null) + this.intersectingObjects = new ArrayList(); + + this.intersectingObjects.addAll(c); + this.objectSector = (this.objectSector != null) ? this.objectSector.union(sector) : sector; + } + + /** + * {@inheritDoc} + *

    + * Overridden to return a new SurfaceObjectTile. The returned tile is created with the same cache name as this + * tile. + */ + @Override + protected SurfaceObjectTile createSubTile(Sector sector, Level level, int row, int col) + { + return new SurfaceObjectTile(sector, level, row, col, this.getCacheName()); + } + + /** + * {@inheritDoc} + *

    + * Overridden to return a TileKey with the same cache name as this tile. + */ + @Override + protected TileKey createSubTileKey(Level level, int row, int col) + { + return new TileKey(level.getLevelNumber(), row, col, this.getCacheName()); + } + } + + /** + * Represents a surface object tile's current state. TileStateKey distinguishes the tile's state by comparing the + * individual state keys of the surface objects intersecting the tile. This does not retain any references to the + * surface objects themselves. Should the tile state key live longer than the surface objects, the state key does + * not prevent those objects from being reclaimed by the garbage collector. + */ + protected static class SurfaceObjectTileStateKey implements Cacheable + { + protected final TileKey tileKey; + protected final Object[] intersectingObjectKeys; + + /** + * Construsts a tile state key for the specified surface object tile. + * + * @param dc the draw context the state key is related to. + * @param tile the tile to construct a state key for. + */ + public SurfaceObjectTileStateKey(DrawContext dc, SurfaceObjectTile tile) + { + if (tile != null && tile.hasObjects()) + { + this.tileKey = tile.getTileKey(); + this.intersectingObjectKeys = new Object[tile.getObjectList().size()]; + + int index = 0; + for (SurfaceObject so : tile.getObjectList()) + { + this.intersectingObjectKeys[index++] = so.getStateKey(dc); + } + } + else + { + this.tileKey = null; + this.intersectingObjectKeys = null; + } + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || this.getClass() != o.getClass()) + return false; + + // Compare the tile keys and each state key in the array. The state keys are equal if the tile keys are + // equal, the arrays equivalent length, and each array element is equivalent. Arrays.equals() correctly + // handles null references. + SurfaceObjectTileStateKey that = (SurfaceObjectTileStateKey) o; + return (this.tileKey != null ? this.tileKey.equals(that.tileKey) : that.tileKey == null) + && Arrays.equals(this.intersectingObjectKeys, that.intersectingObjectKeys); + } + + @Override + public int hashCode() + { + int result = this.tileKey != null ? this.tileKey.hashCode() : 0; + result = 31 * result + Arrays.hashCode(this.intersectingObjectKeys); // Correctly handles a null reference. + return result; + } + + /** + * Returns the tile state key's size in bytes. The total size of the intersecting object keys, plus the size of + * the array itself. The tileKey is owned by the SurfaceObjectTile, so we don't include it in the state key's + * size. + * + * @return The state key's size in bytes. + */ + public long getSizeInBytes() + { + if (this.intersectingObjectKeys == null) + return 0; + + long size = 4 * this.intersectingObjectKeys.length; // For the array references. + + for (Object o : this.intersectingObjectKeys) + { + if (o instanceof Cacheable) + size += ((Cacheable) o).getSizeInBytes(); + else if (o != null) + size += 4; // If the object doesn't implement Cacheable, just account for the reference to it. + } + + return size; + } + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/render/SurfaceQuad.java b/WorldWindAndroid/src/gov/nasa/worldwind/render/SurfaceQuad.java new file mode 100644 index 0000000..96dea97 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/render/SurfaceQuad.java @@ -0,0 +1,560 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.render; + +import android.opengl.GLES20; +import gov.nasa.worldwind.geom.Angle; +import gov.nasa.worldwind.geom.LatLon; +import gov.nasa.worldwind.geom.Position; +import gov.nasa.worldwind.globes.Globe; +import gov.nasa.worldwind.util.BufferUtil; +import gov.nasa.worldwind.util.Logging; +import gov.nasa.worldwind.util.SurfaceTileDrawContext; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static android.opengl.GLES20.GL_FLOAT; +import static android.opengl.GLES20.GL_LINE_STRIP; +import static android.opengl.GLES20.glDrawArrays; + +/** + * @author dcollins + * @version $Id: SurfaceQuad.java 1171 2013-02-11 21:45:02Z dcollins $ + */ +public class SurfaceQuad extends AbstractSurfaceShape +{ + protected LatLon center = LatLon.ZERO; + protected double width; + protected double height; + protected Angle heading = Angle.ZERO; + + /** + * Constructs a new surface quad with the default attributes, default center location, default dimensions, and + * default heading. + */ + public SurfaceQuad() + { + } + + /** + * Constructs a new surface quad with the specified normal (as opposed to highlight) attributes, default center + * location, default dimensions, and default heading. Modifying the attribute reference after calling this + * constructor causes this shape's appearance to change accordingly. + * + * @param normalAttrs the normal attributes. May be null, in which case default attributes are used. + */ + public SurfaceQuad(ShapeAttributes normalAttrs) + { + super(normalAttrs); + } + + /** + * Constructs a new surface quad with the default attributes, the specified center location and dimensions (in + * meters). + * + * @param center the quad's center location. + * @param width the quad's width, in meters. + * @param height the quad's height, in meters. + * + * @throws IllegalArgumentException if the center is null, or if the width or height are negative. + */ + public SurfaceQuad(LatLon center, double width, double height) + { + if (center == null) + { + String message = Logging.getMessage("nullValue.CenterIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (width < 0) + { + String message = Logging.getMessage("Geom.WidthIsNegative", width); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (height < 0) + { + String message = Logging.getMessage("Geom.HeightIsNegative", height); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.center = center; + this.width = width; + this.height = height; + } + + /** + * Constructs a new surface quad with the default attributes, the specified center location, dimensions (in meters), + * and heading clockwise from North. + * + * @param center the quad's center location. + * @param width the quad's width, in meters. + * @param height the quad's height, in meters. + * @param heading the quad's heading, clockwise from North. + * + * @throws IllegalArgumentException if the center or heading are null, or if the width or height are negative. + */ + public SurfaceQuad(LatLon center, double width, double height, Angle heading) + { + this(center, width, height); + + if (heading == null) + { + String message = Logging.getMessage("nullValue.HeadingIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.heading = heading; + } + + /** + * Constructs a new surface quad with the specified normal (as opposed to highlight) attributes, the specified + * center location and dimensions (in meters). Modifying the attribute reference after calling this constructor + * causes this shape's appearance to change accordingly. + * + * @param normalAttrs the normal attributes. May be null, in which case default attributes are used. + * @param center the quad's center location. + * @param width the quad's width, in meters. + * @param height the quad's height, in meters. + * + * @throws IllegalArgumentException if the center is null, or if the width or height are negative. + */ + public SurfaceQuad(ShapeAttributes normalAttrs, LatLon center, double width, double height) + { + super(normalAttrs); + + if (center == null) + { + String message = Logging.getMessage("nullValue.CenterIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (width < 0) + { + String message = Logging.getMessage("Geom.WidthIsNegative", width); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (height < 0) + { + String message = Logging.getMessage("Geom.HeightIsNegative", height); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.center = center; + this.width = width; + this.height = height; + } + + /** + * Constructs a new surface quad with the specified normal (as opposed to highlight) attributes, the specified + * center location and dimensions (in meters). Modifying the attribute reference after calling this constructor + * causes this shape's appearance to change accordingly. + * + * @param normalAttrs the normal attributes. May be null, in which case default attributes are used. + * @param center the quad's center location. + * @param width the quad's width, in meters. + * @param height the quad's height, in meters. + * @param heading the quad's heading, clockwise from North. + * + * @throws IllegalArgumentException if the center or heading are null, or if the width or height are negative. + */ + public SurfaceQuad(ShapeAttributes normalAttrs, LatLon center, double width, double height, Angle heading) + { + this(normalAttrs, center, width, height); + + if (heading == null) + { + String message = Logging.getMessage("nullValue.HeadingIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.heading = heading; + } + + public LatLon getCenter() + { + return this.center; + } + + public void setCenter(LatLon center) + { + if (center == null) + { + String message = Logging.getMessage("nullValue.CenterIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.center = center; + this.onShapeChanged(); + } + + public double getWidth() + { + return this.width; + } + + public double getHeight() + { + return this.height; + } + + public void setWidth(double width) + { + if (width < 0) + { + String message = Logging.getMessage("Geom.WidthIsNegative", width); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.width = width; + this.onShapeChanged(); + } + + public void setHeight(double height) + { + if (height < 0) + { + String message = Logging.getMessage("Geom.HeightIsNegative", height); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.height = height; + this.onShapeChanged(); + } + + public void setSize(double width, double height) + { + if (width < 0) + { + String message = Logging.getMessage("Geom.WidthIsNegative", width); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (height < 0) + { + String message = Logging.getMessage("Geom.HeightIsNegative", height); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.width = width; + this.height = height; + this.onShapeChanged(); + } + + public Angle getHeading() + { + return this.heading; + } + + public void setHeading(Angle heading) + { + if (heading == null) + { + String message = Logging.getMessage("nullValue.HeadingIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.heading = heading; + this.onShapeChanged(); + } + + /** + * {@inheritDoc} + *

    + * Overridden to include the globe's state key in the returned state key. + * + * @see gov.nasa.worldwind.globes.Globe#getStateKey(gov.nasa.worldwind.render.DrawContext) + */ + @Override + public Object getStateKey(DrawContext dc) + { + // Store a copy of the active attributes to insulate the key from changes made to the shape's active attributes. + return new SurfaceShapeStateKey(this.getUniqueId(), this.lastModifiedTime, this.getActiveAttributes().copy(), + dc.getGlobe().getStateKey(dc)); + } + + public Position getReferencePosition() + { + return new Position(this.center, 0); + } + + protected void doMoveTo(Position oldReferencePosition, Position newReferencePosition) + { + Angle heading = LatLon.greatCircleAzimuth(oldReferencePosition, this.center); + Angle pathLength = LatLon.greatCircleDistance(oldReferencePosition, this.center); + this.setCenter(LatLon.greatCircleEndPosition(newReferencePosition, heading, pathLength)); + } + + public Iterable getLocations(Globe globe) + { + if (globe == null) + { + String message = Logging.getMessage("nullValue.GlobeIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (this.width == 0 && this.height == 0) + return null; + + double hw = this.width / 2.0; + double hh = this.height / 2.0; + double globeRadius = globe.getRadiusAt(this.center.getLatitude(), this.center.getLongitude()); + double distance = Math.sqrt(hw * hw + hh * hh); + double pathLength = distance / globeRadius; + + double[] cornerAngles = new double[] + { + Math.atan2(-hh, -hw), + Math.atan2(-hh, hw), + Math.atan2(hh, hw), + Math.atan2(hh, -hw), + Math.atan2(-hh, -hw), + }; + + LatLon[] locations = new LatLon[cornerAngles.length]; + + for (int i = 0; i < cornerAngles.length; i++) + { + double azimuth = (Math.PI / 2.0) - (cornerAngles[i] - this.heading.radians); + locations[i] = LatLon.greatCircleEndPosition(this.center, azimuth, pathLength); + } + + return Arrays.asList(locations); + } + + protected List> createGeometry(Globe globe, SurfaceTileDrawContext sdc) + { + if (globe == null) + { + String message = Logging.getMessage("nullValue.GlobeIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + Iterable originalLocations = this.getLocations(globe); + if (originalLocations == null) + return null; + + ArrayList drawLocations = new ArrayList(); + double edgeIntervalsPerDegree = this.computeEdgeIntervalsPerDegree(sdc); + this.generateIntermediateLocations(originalLocations, edgeIntervalsPerDegree, false, drawLocations); + + ArrayList> geom = new ArrayList>(); + geom.add(drawLocations); + + return geom; + } + + @Override + protected Integer doTessellateInterior(DrawContext dc) { + Position refPos = this.getReferencePosition(); + if (refPos == null) + return null; + + for (List drawLocations : this.activeGeometry) + { + if (vertexBuffer == null || vertexBuffer.capacity() < 2 * drawLocations.size()) + vertexBuffer = BufferUtil.newFloatBuffer(2 * drawLocations.size()); + vertexBuffer.clear(); + + for (LatLon ll : drawLocations) + { + vertexBuffer.put((float) (ll.getLongitude().degrees - refPos.getLongitude().degrees)); + vertexBuffer.put((float) (ll.getLatitude().degrees - refPos.getLatitude().degrees)); + } + vertexBuffer.flip(); + + dc.getCurrentProgram().vertexAttribPointer("vertexPoint", 2, GL_FLOAT, false, 0, vertexBuffer); + glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, drawLocations.size()); + } + return 4*8; + } + +//**************************************************************// + //******************** Restorable State ***********************// + //**************************************************************// + +// protected void doGetRestorableState(RestorableSupport rs, RestorableSupport.StateObject context) +// { +// super.doGetRestorableState(rs, context); +// +// rs.addStateValueAsLatLon(context, "center", this.getCenter()); +// rs.addStateValueAsDouble(context, "width", this.getWidth()); +// rs.addStateValueAsDouble(context, "height", this.getHeight()); +// rs.addStateValueAsDouble(context, "headingDegrees", this.getHeading().degrees); +// } +// +// protected void doRestoreState(RestorableSupport rs, RestorableSupport.StateObject context) +// { +// super.doRestoreState(rs, context); +// +// LatLon ll = rs.getStateValueAsLatLon(context, "center"); +// if (ll != null) +// this.setCenter(ll); +// +// Double d = rs.getStateValueAsDouble(context, "width"); +// if (d != null) +// this.setWidth(d); +// +// d = rs.getStateValueAsDouble(context, "height"); +// if (d != null) +// this.setHeight(d); +// +// d = rs.getStateValueAsDouble(context, "headingDegrees"); +// if (d != null) +// this.setHeading(Angle.fromDegrees(d)); +// } +// +// protected void legacyRestoreState(RestorableSupport rs, RestorableSupport.StateObject context) +// { +// super.legacyRestoreState(rs, context); +// +// // Previous versions of SurfaceQuad used half-width and half-height properties. We are now using standard +// // width and height, so these restored values must be converted. +// Double width = rs.getStateValueAsDouble(context, "halfWidth"); +// Double height = rs.getStateValueAsDouble(context, "halfHeight"); +// if (width != null && height != null) +// this.setSize(2 * width, 2 * height); +// +// // This property has not changed since the previos version, but it's shown here for reference. +// //LatLon center = rs.getStateValueAsLatLon(context, "center"); +// //if (center != null) +// // this.setCenter(center); +// +// Double od = rs.getStateValueAsDouble(context, "orientationDegrees"); +// if (od != null) +// this.setHeading(Angle.fromDegrees(od)); +// } +// +// /** +// * Export the polygon to KML as a {@code } element. The {@code output} object will receive the data. This +// * object must be one of: java.io.Writer java.io.OutputStream javax.xml.stream.XMLStreamWriter +// * +// * @param output Object to receive the generated KML. +// * +// * @throws javax.xml.stream.XMLStreamException If an exception occurs while writing the KML +// * @throws java.io.IOException if an exception occurs while exporting the data. +// * @see #export(String, Object) +// */ +// protected void exportAsKML(Object output) throws IOException, XMLStreamException +// { +// XMLStreamWriter xmlWriter = null; +// XMLOutputFactory factory = XMLOutputFactory.newInstance(); +// boolean closeWriterWhenFinished = true; +// +// if (output instanceof XMLStreamWriter) +// { +// xmlWriter = (XMLStreamWriter) output; +// closeWriterWhenFinished = false; +// } +// else if (output instanceof Writer) +// { +// xmlWriter = factory.createXMLStreamWriter((Writer) output); +// } +// else if (output instanceof OutputStream) +// { +// xmlWriter = factory.createXMLStreamWriter((OutputStream) output); +// } +// +// if (xmlWriter == null) +// { +// String message = Logging.getMessage("Export.UnsupportedOutputObject"); +// Logging.logger().warning(message); +// throw new IllegalArgumentException(message); +// } +// +// xmlWriter.writeStartElement("Placemark"); +// +// String property = (String) getValue(AVKey.DISPLAY_NAME); +// if (property != null) +// { +// xmlWriter.writeStartElement("name"); +// xmlWriter.writeCharacters(property); +// xmlWriter.writeEndElement(); +// } +// +// xmlWriter.writeStartElement("visibility"); +// xmlWriter.writeCharacters(KMLExportUtil.kmlBoolean(this.isVisible())); +// xmlWriter.writeEndElement(); +// +// String shortDescription = (String) getValue(AVKey.SHORT_DESCRIPTION); +// if (shortDescription != null) +// { +// xmlWriter.writeStartElement("Snippet"); +// xmlWriter.writeCharacters(shortDescription); +// xmlWriter.writeEndElement(); +// } +// +// String description = (String) getValue(AVKey.BALLOON_TEXT); +// if (description != null) +// { +// xmlWriter.writeStartElement("description"); +// xmlWriter.writeCharacters(description); +// xmlWriter.writeEndElement(); +// } +// +// // KML does not allow separate attributes for cap and side, so just use the side attributes. +// final ShapeAttributes normalAttributes = getAttributes(); +// final ShapeAttributes highlightAttributes = getHighlightAttributes(); +// +// // Write style map +// if (normalAttributes != null || highlightAttributes != null) +// { +// xmlWriter.writeStartElement("StyleMap"); +// KMLExportUtil.exportAttributesAsKML(xmlWriter, KMLConstants.NORMAL, normalAttributes); +// KMLExportUtil.exportAttributesAsKML(xmlWriter, KMLConstants.HIGHLIGHT, highlightAttributes); +// xmlWriter.writeEndElement(); // StyleMap +// } +// +// // Write geometry +// xmlWriter.writeStartElement("Polygon"); +// +// xmlWriter.writeStartElement("extrude"); +// xmlWriter.writeCharacters("0"); +// xmlWriter.writeEndElement(); +// +// xmlWriter.writeStartElement("altitudeMode"); +// xmlWriter.writeCharacters("clampToGround"); +// xmlWriter.writeEndElement(); +// +// String globeName = Configuration.getStringValue(AVKey.GLOBE_CLASS_NAME, "gov.nasa.worldwind.globes.Earth"); +// Globe globe = (Globe) WorldWind.createComponent(globeName); +// +// // Outer boundary +// Iterable outerBoundary = this.getLocations(globe); +// if (outerBoundary != null) +// { +// xmlWriter.writeStartElement("outerBoundaryIs"); +// KMLExportUtil.exportBoundaryAsLinearRing(xmlWriter, outerBoundary, null); +// xmlWriter.writeEndElement(); // outerBoundaryIs +// } +// +// xmlWriter.writeEndElement(); // Polygon +// xmlWriter.writeEndElement(); // Placemark +// +// xmlWriter.flush(); +// if (closeWriterWhenFinished) +// xmlWriter.close(); +// } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/render/SurfaceShape.java b/WorldWindAndroid/src/gov/nasa/worldwind/render/SurfaceShape.java new file mode 100644 index 0000000..af2c15b --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/render/SurfaceShape.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2014 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.geom.ExtentHolder; +import gov.nasa.worldwind.geom.LatLon; +import gov.nasa.worldwind.geom.MeasurableArea; +import gov.nasa.worldwind.geom.MeasurableLength; +import gov.nasa.worldwind.globes.Globe; + +/** + * Common interface for surface conforming shapes such as {@link gov.nasa.worldwind.render.SurfacePolygon}, {@link + * gov.nasa.worldwind.render.SurfacePolyline}, {@link gov.nasa.worldwind.render.SurfaceEllipse}, {@link + * gov.nasa.worldwind.render.SurfaceQuad}, and {@link gov.nasa.worldwind.render.SurfaceSector}. + *

    + * SurfaceShape extends the {@link gov.nasa.worldwind.render.SurfaceObject} interface, and inherits SurfaceObject's + * batch rendering capabilities. + * + * @author dcollins + * @version $Id: SurfaceShape.java 1171 2013-02-11 21:45:02Z dcollins $ + */ +public interface SurfaceShape + extends SurfaceObject, Highlightable, ExtentHolder, MeasurableArea, MeasurableLength +{ + /** + * Indicates whether to highlight the surface shape. + * + * @return true if highlighted, otherwise false. + * + * @see #setHighlighted(boolean) + * @see #setHighlightAttributes(gov.nasa.worldwind.render.ShapeAttributes) + */ + boolean isHighlighted(); + + /** + * Specifies whether the surface shape is highlighted. + * + * @param highlighted true to highlight the surface shape, otherwise false. The default value is false. + * + * @see #setHighlightAttributes(gov.nasa.worldwind.render.ShapeAttributes) + */ + void setHighlighted(boolean highlighted); + + /** + * Returns the surface shape's normal (as opposed to highlight) attributes. Modifying the contents of the returned + * reference causes this shape's appearance to change accordingly. + * + * @return the surface shape's normal attributes. May be null. + */ + ShapeAttributes getAttributes(); + + /** + * Specifies the surface shape's normal (as opposed to highlight) attributes. Modifying the attribute reference + * after calling setAttributes() causes this shape's appearance to change accordingly. + * + * @param normalAttrs the normal attributes. May be null, in which case default attributes are used. + */ + void setAttributes(ShapeAttributes normalAttrs); + + /** + * Returns the surface shape's highlight attributes. Modifying the contents of the returned reference causes this + * shape's appearance to change accordingly. + * + * @return the surface shape's highlight attributes. May be null. + */ + ShapeAttributes getHighlightAttributes(); + + /** + * Specifies the surface shape's highlight attributes. Modifying the attribute reference after calling + * setHighlightAttributes() causes this shape's appearance to change accordingly. + * + * @param highlightAttrs the highlight attributes. May be null, in which case default attributes are used. + */ + void setHighlightAttributes(ShapeAttributes highlightAttrs); + + /** + * Returns the path type used to interpolate between locations on this SurfaceShape. + * + * @return path interpolation type. + */ + String getPathType(); + + /** + * Sets the path type used to interpolate between locations on this SurfaceShape. This should be one of

      + *
    • gov.nasa.worldwind.avlist.AVKey.GREAT_CIRCLE
    • gov.nasa.worldwind.avlist.AVKey.LINEAR
    • + *
    • gov.nasa.worldwind.avlist.AVKey.LOXODROME
    • gov.nasa.worldwind.avlist.AVKey.RHUMB
    + * + * @param pathType path interpolation type. + * + * @throws IllegalArgumentException if pathType is null. + */ + void setPathType(String pathType); + + /** + * Returns the number of texels per shape edge interval. + * + * @return texels per shape edge interval. + * + * @see #setTexelsPerEdgeInterval(double) + */ + double getTexelsPerEdgeInterval(); + + /** + * Sets the number of texels per shape edge interval. This value controls how many interpolated intervals are added + * to each shape edge, depending on size of the original edge in texels. Each shape is responsible for defining what + * an edge is, though for most shapes it is defined as the edge between implicit or caller-specified shape + * locations. The number of interpolated intervals is limited by the values set in a call to {@link + * #setMinAndMaxEdgeIntervals(int, int)}. + * + * @param texelsPerEdgeInterval the size, in texels, of each interpolated edge interval. + * + * @throws IllegalArgumentException if texelsPerEdgeInterval is less than or equal to zero. + * @see #setMinAndMaxEdgeIntervals(int, int) + */ + void setTexelsPerEdgeInterval(double texelsPerEdgeInterval); + + /** + * Returns the minimum and maximum number of interpolated intervals that may be added to each shape edge. + * + * @return array of two elements, the first element is minEdgeIntervals, the second element is maxEdgeIntervals. + * + * @see #setMinAndMaxEdgeIntervals(int, int) + */ + int[] getMinAndMaxEdgeIntervals(); + + /** + * Sets the minimum and maximum number of interpolated intervals that may be added to each shape edge. The minimum + * and maximum values may be 0, or any positive integer. Note that Setting either of minEdgeIntervals + * or maxEdgeIntervals too large may adversely impact surface shape rendering performance. + * + * @param minEdgeIntervals the minimum number of interpolated edge intervals. + * @param maxEdgeIntervals the maximum number of interpolated edge intervals. + * + * @throws IllegalArgumentException if either of minEdgeIntervals or maxEdgeIntervals is + * less than or equal to zero. + * @see #setTexelsPerEdgeInterval(double) + */ + void setMinAndMaxEdgeIntervals(int minEdgeIntervals, int maxEdgeIntervals); + + /** + * Returns the shape's locations as they appear on the specified globe, or null if the shape has no + * locations. + * + * @param globe the globe the shape is related to. + * + * @return the shapes locations on the globe, or null if the shape has no locations. + * + * @throws IllegalArgumentException if globe is null. + */ + Iterable getLocations(Globe globe); + + /** + * Returns the shapes's area in square meters. If terrainConformant is true, the area returned is the + * surface area of the terrain, including its hillsides and other undulations. + * + * @param globe the globe the shape is related to. + * @param terrainConformant whether or not the returned area should treat the shape as conforming to the terrain. + * + * @return the shape's area in square meters. Returns -1 if the object does not form an area due to an insufficient + * number of vertices or any other condition. + * + * @throws IllegalArgumentException if globe is null. + */ + double getArea(Globe globe, boolean terrainConformant); +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/render/SurfaceTileRenderer.java b/WorldWindAndroid/src/gov/nasa/worldwind/render/SurfaceTileRenderer.java index 0baec66..85d80fb 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/render/SurfaceTileRenderer.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/render/SurfaceTileRenderer.java @@ -6,12 +6,17 @@ package gov.nasa.worldwind.render; import android.opengl.GLES20; +import gov.nasa.worldwind.R; +import gov.nasa.worldwind.WorldWindowImpl; import gov.nasa.worldwind.cache.GpuResourceCache; -import gov.nasa.worldwind.geom.*; -import gov.nasa.worldwind.terrain.*; +import gov.nasa.worldwind.geom.Matrix; +import gov.nasa.worldwind.geom.Sector; +import gov.nasa.worldwind.terrain.SectorGeometry; +import gov.nasa.worldwind.terrain.SectorGeometryList; import gov.nasa.worldwind.util.Logging; -import java.util.*; +import java.util.ArrayList; +import java.util.List; /** * @author dcollins @@ -19,263 +24,268 @@ */ public class SurfaceTileRenderer { - protected static final String VERTEX_SHADER_PATH = "shaders/SurfaceTileRenderer.vert"; - protected static final String FRAGMENT_SHADER_PATH = "shaders/SurfaceTileRenderer.frag"; - - protected final Object programKey = new Object(); - protected boolean programCreationFailed; - protected List intersectingTiles = new ArrayList(); - protected List intersectingGeometry = new ArrayList(); - /** - * Matrix defining the tile coordinate transform matrix. Maps normalized surface texture coordinates to normalized - * tile coordinates. - */ - protected Matrix tileCoordMatrix = Matrix.fromIdentity(); - /** - * Matrix defining the texture coordinate transform matrix. Maps normalized surface texture coordinates to tile - * texture coordinates. - */ - protected Matrix texCoordMatrix = Matrix.fromIdentity(); - - public SurfaceTileRenderer() - { - } - - public void renderTile(DrawContext dc, SurfaceTile tile) - { - if (dc == null) - { - String msg = Logging.getMessage("nullValue.DrawContextIsNull"); - Logging.error(msg); - throw new IllegalStateException(msg); - } - - if (tile == null) - { - String msg = Logging.getMessage("nullValue.TileIsNull"); - Logging.error(msg); - throw new IllegalStateException(msg); - } - - SectorGeometryList sgList = dc.getSurfaceGeometry(); - if (sgList == null) - { - Logging.warning(Logging.getMessage("generic.NoSurfaceGeometry")); - return; - } - - GpuProgram program = this.getGpuProgram(dc.getGpuResourceCache()); - if (program == null) - return; // Message already logged in getGpuProgram. - - this.beginRendering(dc, program); - sgList.beginRendering(dc); - try - { - if (tile.bind(dc)) - { - this.intersectingGeometry.clear(); - this.assembleIntersectingGeometry(tile, sgList); - - for (int i = 0; i < this.intersectingGeometry.size(); i++) - { - SectorGeometry sg = this.intersectingGeometry.get(i); - - sg.beginRendering(dc); - try - { - this.applyTileState(dc, sg, tile); - sg.render(dc); - } - finally - { - sg.endRendering(dc); - } - } - } - } - finally - { - sgList.endRendering(dc); - this.endRendering(dc); - } - } - - public void renderTiles(DrawContext dc, List tiles) - { - if (dc == null) - { - String msg = Logging.getMessage("nullValue.DrawContextIsNull"); - Logging.error(msg); - throw new IllegalStateException(msg); - } - - if (tiles == null) - { - String msg = Logging.getMessage("nullValue.TileListIsNull"); - Logging.error(msg); - throw new IllegalStateException(msg); - } - - SectorGeometryList sgList = dc.getSurfaceGeometry(); - if (sgList == null) - { - Logging.warning(Logging.getMessage("generic.NoSurfaceGeometry")); - return; - } - - GpuProgram program = this.getGpuProgram(dc.getGpuResourceCache()); - if (program == null) - return; // Exception logged in loadGpuProgram. - - this.beginRendering(dc, program); - sgList.beginRendering(dc); - try - { - for (int i = 0; i < sgList.size(); i++) - { - SectorGeometry sg = sgList.get(i); - - this.intersectingTiles.clear(); - this.assembleIntersectingTiles(sg, tiles); - if (this.intersectingTiles.isEmpty()) - continue; // Nothing to draw if the tiles don't intersect this surface geometry. - - sg.beginRendering(dc); - try - { - for (int j = 0; j < this.intersectingTiles.size(); j++) - { - SurfaceTile tile = this.intersectingTiles.get(j); - if (tile.bind(dc)) - { - this.applyTileState(dc, sg, tile); - sg.render(dc); - } - } - } - finally - { - sg.endRendering(dc); - } - } - } - finally - { - sgList.endRendering(dc); - this.endRendering(dc); - } - } - - protected void beginRendering(DrawContext dc, GpuProgram program) - { - // Bind this SurfaceTileRenderer's Gpu program as the current program. - program.bind(); - dc.setCurrentProgram(program); - // Specify that the tile textures are bound to texture unit GL_TEXTURE0. - GLES20.glActiveTexture(GLES20.GL_TEXTURE0); - program.loadUniformSampler("tileTexture", 0); - } - - protected void endRendering(DrawContext dc) - { - dc.setCurrentProgram(null); - // Restore the current program to 0 and the active texture unit to GL_TEXTURE0. - GLES20.glUseProgram(0); - GLES20.glActiveTexture(GLES20.GL_TEXTURE0); - - // Clear the list of intersecting tiles to avoid keeping references to caller specified tiles. - this.intersectingTiles.clear(); - this.intersectingGeometry.clear(); - } - - protected void assembleIntersectingTiles(SectorGeometry sg, List tiles) - { - Sector sgSector = sg.getSector(); - - for (int i = 0; i < tiles.size(); i++) - { - SurfaceTile tile = tiles.get(i); - - if (tile != null && tile.getSector().intersectsInterior(sgSector)) - this.intersectingTiles.add(tile); - } - } - - protected void assembleIntersectingGeometry(SurfaceTile tile, List sgList) - { - Sector tileSector = tile.getSector(); - - for (int i = 0; i < sgList.size(); i++) - { - SectorGeometry sg = sgList.get(i); - - if (sg != null && sg.getSector().intersects(tileSector)) - this.intersectingGeometry.add(sg); - } - } - - protected void applyTileState(DrawContext dc, SectorGeometry sg, SurfaceTile tile) - { - GpuProgram program = dc.getCurrentProgram(); - if (program == null) - return; // Message logged in loadGpuProgram. - - this.computeTileCoordMatrix(sg, tile, this.tileCoordMatrix); - program.loadUniformMatrix("tileCoordMatrix", this.tileCoordMatrix); - - this.texCoordMatrix.setIdentity(); - tile.applyInternalTransform(dc, this.texCoordMatrix); - this.texCoordMatrix.multiplyAndSet(this.tileCoordMatrix); - program.loadUniformMatrix("texCoordMatrix", this.texCoordMatrix); - } - - protected void computeTileCoordMatrix(SectorGeometry sg, SurfaceTile tile, Matrix result) - { - Sector sgSector = sg.getSector(); - double sgDeltaLon = sgSector.getDeltaLonRadians(); - double sgDeltaLat = sgSector.getDeltaLatRadians(); - - Sector tileSector = tile.getSector(); - double tileDeltaLon = tileSector.getDeltaLonRadians(); - double tileDeltaLat = tileSector.getDeltaLatRadians(); - - double sScale = tileDeltaLon > 0 ? sgDeltaLon / tileDeltaLon : 1; - double tScale = tileDeltaLon > 0 ? sgDeltaLat / tileDeltaLat : 1; - double sTrans = -(tileSector.minLongitude.radians - sgSector.minLongitude.radians) / sgDeltaLon; - double tTrans = -(tileSector.minLatitude.radians - sgSector.minLatitude.radians) / sgDeltaLat; - - result.set( - sScale, 0, 0, sScale * sTrans, - 0, tScale, 0, tScale * tTrans, - 0, 0, 1, 0, - 0, 0, 0, 1); - } - - protected GpuProgram getGpuProgram(GpuResourceCache cache) - { - if (this.programCreationFailed) - return null; - - GpuProgram program = cache.getProgram(this.programKey); - - if (program == null) - { - try - { - GpuProgram.GpuProgramSource source = GpuProgram.readProgramSource(VERTEX_SHADER_PATH, - FRAGMENT_SHADER_PATH); - program = new GpuProgram(source); - cache.put(this.programKey, program); - } - catch (Exception e) - { - String msg = Logging.getMessage("GL.ExceptionLoadingProgram", VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH); - Logging.error(msg); - this.programCreationFailed = true; - } - } - - return program; - } + protected static final int VERTEX_SHADER_PATH = R.raw.surfacetilerenderervert; + protected static final int FRAGMENT_SHADER_PATH = R.raw.surfacetilerendererfrag; + + protected final Object programKey = new Object(); + protected boolean programCreationFailed; + protected List intersectingTiles = new ArrayList(); + protected List intersectingGeometry = new ArrayList(); + /** + * Matrix defining the tile coordinate transform matrix. Maps normalized surface texture coordinates to normalized + * tile coordinates. + */ + protected Matrix tileCoordMatrix = Matrix.fromIdentity(); + /** + * Matrix defining the texture coordinate transform matrix. Maps normalized surface texture coordinates to tile + * texture coordinates. + */ + protected Matrix texCoordMatrix = Matrix.fromIdentity(); + + public SurfaceTileRenderer() + { + } + + public void renderTile(DrawContext dc, SurfaceTile tile) + { + if (dc == null) + { + String msg = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(msg); + throw new IllegalStateException(msg); + } + + if (tile == null) + { + String msg = Logging.getMessage("nullValue.TileIsNull"); + Logging.error(msg); + throw new IllegalStateException(msg); + } + + SectorGeometryList sgList = dc.getSurfaceGeometry(); + if (sgList == null) + { + Logging.warning(Logging.getMessage("generic.NoSurfaceGeometry")); + return; + } + + GpuProgram program = this.getGpuProgram(dc.getGpuResourceCache()); + if (program == null) + return; // Message already logged in getGpuProgram. + + this.beginRendering(dc, program); + sgList.beginRendering(dc); + try + { + if (tile.bind(dc)) + { + this.intersectingGeometry.clear(); + this.assembleIntersectingGeometry(tile, sgList); + + for (int i = 0; i < this.intersectingGeometry.size(); i++) + { + SectorGeometry sg = this.intersectingGeometry.get(i); + + sg.beginRendering(dc); + try + { + this.applyTileState(dc, sg, tile); + sg.render(dc); + } + finally + { + sg.endRendering(dc); + } + } + } + } + finally + { + sgList.endRendering(dc); + this.endRendering(dc); + } + } + + public void renderTiles(DrawContext dc, List tiles) + { + if (dc == null) + { + String msg = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(msg); + throw new IllegalStateException(msg); + } + + if (tiles == null) + { + String msg = Logging.getMessage("nullValue.TileListIsNull"); + Logging.error(msg); + throw new IllegalStateException(msg); + } + + SectorGeometryList sgList = dc.getSurfaceGeometry(); + if (sgList == null) + { + Logging.warning(Logging.getMessage("generic.NoSurfaceGeometry")); + return; + } + + GpuProgram program = this.getGpuProgram(dc.getGpuResourceCache()); + if (program == null) + return; // Exception logged in loadGpuProgram. + + this.beginRendering(dc, program); + sgList.beginRendering(dc); + try + { + for (int i = 0; i < sgList.size(); i++) + { + SectorGeometry sg = sgList.get(i); + + this.intersectingTiles.clear(); + this.assembleIntersectingTiles(sg, tiles); + if (this.intersectingTiles.isEmpty()) + continue; // Nothing to draw if the tiles don't intersect this surface geometry. + + sg.beginRendering(dc); + try + { + for (int j = 0; j < this.intersectingTiles.size(); j++) + { + SurfaceTile tile = this.intersectingTiles.get(j); + if (tile.bind(dc)) + { + this.applyTileState(dc, sg, tile); + sg.render(dc); + } + } + } + finally + { + sg.endRendering(dc); + } + } + } + finally + { + sgList.endRendering(dc); + this.endRendering(dc); + } + } + + protected void beginRendering(DrawContext dc, GpuProgram program) + { + // Bind this SurfaceTileRenderer's Gpu program as the current program. + program.bind(); + dc.setCurrentProgram(program); + // Specify that the tile textures are bound to texture unit GL_TEXTURE0. + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + WorldWindowImpl.glCheckError("glActiveTexture"); + program.loadUniformSampler("tileTexture", 0); + //SurfaceObject tiles are drawn as composite, outside of their parent layer. In this case layer opacity + //is applied to each individual tile when they are created. + program.loadUniform1f("uOpacity", (dc.isPickingMode() || dc.getCurrentLayer()==null) ? 1f : dc.getCurrentLayer().getOpacity()); + } + + protected void endRendering(DrawContext dc) + { + dc.setCurrentProgram(null); + // Restore the current program to 0 and the active texture unit to GL_TEXTURE0. + GLES20.glUseProgram(0); + WorldWindowImpl.glCheckError("glUseProgram"); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + WorldWindowImpl.glCheckError("glActiveTexture"); + + // Clear the list of intersecting tiles to avoid keeping references to caller specified tiles. + this.intersectingTiles.clear(); + this.intersectingGeometry.clear(); + } + + protected void assembleIntersectingTiles(SectorGeometry sg, List tiles) + { + Sector sgSector = sg.getSector(); + + for (int i = 0; i < tiles.size(); i++) + { + SurfaceTile tile = tiles.get(i); + + if (tile != null && tile.getSector().intersectsInterior(sgSector)) + this.intersectingTiles.add(tile); + } + } + + protected void assembleIntersectingGeometry(SurfaceTile tile, List sgList) + { + Sector tileSector = tile.getSector(); + + for (int i = 0; i < sgList.size(); i++) + { + SectorGeometry sg = sgList.get(i); + + if (sg != null && sg.getSector().intersects(tileSector)) + this.intersectingGeometry.add(sg); + } + } + + protected void applyTileState(DrawContext dc, SectorGeometry sg, SurfaceTile tile) + { + GpuProgram program = dc.getCurrentProgram(); + if (program == null) + return; // Message logged in loadGpuProgram. + + this.computeTileCoordMatrix(sg, tile, this.tileCoordMatrix); + program.loadUniformMatrix("tileCoordMatrix", this.tileCoordMatrix); + + this.texCoordMatrix.setIdentity(); + tile.applyInternalTransform(dc, this.texCoordMatrix); + this.texCoordMatrix.multiplyAndSet(this.tileCoordMatrix); + program.loadUniformMatrix("texCoordMatrix", this.texCoordMatrix); + } + + protected void computeTileCoordMatrix(SectorGeometry sg, SurfaceTile tile, Matrix result) + { + Sector sgSector = sg.getSector(); + double sgDeltaLon = sgSector.getDeltaLonRadians(); + double sgDeltaLat = sgSector.getDeltaLatRadians(); + + Sector tileSector = tile.getSector(); + double tileDeltaLon = tileSector.getDeltaLonRadians(); + double tileDeltaLat = tileSector.getDeltaLatRadians(); + + double sScale = tileDeltaLon > 0 ? sgDeltaLon / tileDeltaLon : 1; + double tScale = tileDeltaLon > 0 ? sgDeltaLat / tileDeltaLat : 1; + double sTrans = -(tileSector.minLongitude.radians - sgSector.minLongitude.radians) / sgDeltaLon; + double tTrans = -(tileSector.minLatitude.radians - sgSector.minLatitude.radians) / sgDeltaLat; + + result.set( + sScale, 0, 0, sScale * sTrans, + 0, tScale, 0, tScale * tTrans, + 0, 0, 1, 0, + 0, 0, 0, 1); + } + + protected GpuProgram getGpuProgram(GpuResourceCache cache) + { + if (this.programCreationFailed) + return null; + + GpuProgram program = cache.getProgram(this.programKey); + + if (program == null) + { + try + { + GpuProgram.GpuProgramSource source = GpuProgram.readProgramSource(VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH); + program = new GpuProgram(source); + cache.put(this.programKey, program); + } + catch (Exception e) + { + String msg = Logging.getMessage("GL.ExceptionLoadingProgram", VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH); + Logging.error(msg); + this.programCreationFailed = true; + } + } + + return program; + } } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/render/TextRenderer.java b/WorldWindAndroid/src/gov/nasa/worldwind/render/TextRenderer.java index f085ad7..7f342f6 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/render/TextRenderer.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/render/TextRenderer.java @@ -1,28 +1,31 @@ package gov.nasa.worldwind.render; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.opengl.GLES20; +import gov.nasa.worldwind.R; +import gov.nasa.worldwind.WorldWindowImpl; import gov.nasa.worldwind.cache.GpuResourceCache; import gov.nasa.worldwind.geom.Matrix; import gov.nasa.worldwind.geom.Rect; import gov.nasa.worldwind.util.Logging; + import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import java.util.HashMap; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.opengl.GLES20; /** * Class used to render text on view - * + * * @author Nicola Dorigatti Trilogis SRL * @version 1 */ public class TextRenderer { - protected static final String VERTEX_SHADER_PATH = "shaders/TextRenderer.vert"; - protected static final String FRAGMENT_SHADER_PATH = "shaders/TextRenderer.frag"; + protected static final int VERTEX_SHADER_PATH = R.raw.textrenderervert; + protected static final int FRAGMENT_SHADER_PATH = R.raw.textrendererfrag; protected static final Object shaderKey = new Object(); private HashMap textKeys = new HashMap(); @@ -30,9 +33,26 @@ public class TextRenderer { private Paint paint; private float[] color = new float[] { 1, 1, 1, 1 }; + float[] unitQuadVerts = new float[] { 0, 0, 1, 0, 1, 1, 0, 1 }; + float[] textureVerts = new float[] { 0, 1, 1, 1, 1, 0, 0, 0 }; + FloatBuffer vertexBuf; + FloatBuffer textureBuf; + + public TextRenderer(DrawContext dc) { + this(dc, null); + } + public TextRenderer(DrawContext dc, Paint paint) { this.drawContext = dc; this.paint = paint; + if(paint==null) { + this.paint = new Paint(); + this.paint.setColor(0xFFFFFFFF); + } + vertexBuf = ByteBuffer.allocateDirect(unitQuadVerts.length * 4).order(ByteOrder.nativeOrder()) + .asFloatBuffer().put(unitQuadVerts); + textureBuf = ByteBuffer.allocateDirect(textureVerts.length * 4).order(ByteOrder.nativeOrder()) + .asFloatBuffer().put(textureVerts); } public Rect getBounds(String text) { @@ -46,6 +66,10 @@ public void setColor(float[] color) { this.color = color; } + public Paint getPaint() { + return this.paint; + } + public void draw(String text, int x, int y) { Rect viewport = drawContext.getView().getViewport(); Rect bounds = getBounds(text); @@ -57,32 +81,40 @@ public void draw(String text, int x, int y) { GpuProgram program = this.getGpuProgram(drawContext.getGpuResourceCache(), shaderKey, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH); program.bind(); program.loadUniformMatrix("mvpMatrix", mvp); - GLES20.glEnable(GLES20.GL_TEXTURE_2D); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + WorldWindowImpl.glCheckError("glActiveTexture"); + GpuTexture texture = getGpuTexture(text); texture.bind(); - program.loadUniform4f("uTextureColor", color[0], color[1], color[2], color[3]); + program.loadUniform4f("uTextureColor", color[0], color[1], color[2], color[3]*drawContext.getCurrentLayer().getOpacity()); program.loadUniformSampler("sTexture", 0); - float[] unitQuadVerts = new float[] { 0, 0, 1, 0, 1, 1, 0, 1 }; int pointLocation = program.getAttribLocation("vertexPoint"); GLES20.glEnableVertexAttribArray(pointLocation); - FloatBuffer vertexBuf = ByteBuffer.allocateDirect(unitQuadVerts.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); - vertexBuf.put(unitQuadVerts); - vertexBuf.rewind(); - GLES20.glVertexAttribPointer(pointLocation, 2, GLES20.GL_FLOAT, false, 0, vertexBuf); - float[] textureVerts = new float[] { 0, 1, 1, 1, 1, 0, 0, 0 }; + WorldWindowImpl.glCheckError("glEnableVertexAttribArray"); + + GLES20.glVertexAttribPointer(pointLocation, 2, GLES20.GL_FLOAT, false, 0, vertexBuf.rewind()); + WorldWindowImpl.glCheckError("glVertexAttribPointer"); + int textureLocation = program.getAttribLocation("aTextureCoord"); GLES20.glEnableVertexAttribArray(textureLocation); - FloatBuffer textureBuf = ByteBuffer.allocateDirect(textureVerts.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); - textureBuf.put(textureVerts); - textureBuf.rewind(); - GLES20.glVertexAttribPointer(textureLocation, 2, GLES20.GL_FLOAT, false, 0, textureBuf); + WorldWindowImpl.glCheckError("glEnableVertexAttribArray"); + + GLES20.glVertexAttribPointer(textureLocation, 2, GLES20.GL_FLOAT, false, 0, textureBuf.rewind()); + WorldWindowImpl.glCheckError("glVertexAttribPointer"); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, unitQuadVerts.length / 2); + WorldWindowImpl.glCheckError("glDrawArrays"); + GLES20.glDisableVertexAttribArray(pointLocation); + WorldWindowImpl.glCheckError("glDisableVertexAttribArray"); + GLES20.glDisableVertexAttribArray(textureLocation); + WorldWindowImpl.glCheckError("glDisableVertexAttribArray"); + GLES20.glUseProgram(0); - GLES20.glDisable(GLES20.GL_TEXTURE_2D); + WorldWindowImpl.glCheckError("glUseProgram"); } protected GpuTexture getGpuTexture(String text) { @@ -97,8 +129,8 @@ protected GpuTexture getGpuTexture(String text) { // TODO: load the texture on a non-rendering thread. texture = this.loadTextTexture(text); if (texture != null) // Don't add the texture to the cache if - // texture creation failed. - cache.put(key, texture); + // texture creation failed. + cache.put(key, texture); } return texture; @@ -112,7 +144,7 @@ private GpuTexture loadTextTexture(String text) { canvas.drawText(text, 0, bounds.height(), paint); GpuTexture texture = null; - GpuTextureData textureData = GpuTextureData.createTextureData(image); + GpuTextureData textureData = GpuTextureData.createTextureData(image, null, null, false); // GpuTextureData textureData = BasicGpuTextureFactory.createTextureData(AVKey.GPU_TEXTURE_FACTORY, image, null); if (textureData != null) { texture = GpuTexture.createTexture(drawContext, textureData); @@ -121,7 +153,7 @@ private GpuTexture loadTextTexture(String text) { return texture; } - protected GpuProgram getGpuProgram(GpuResourceCache cache, Object programKey, String shaderPath, String fragmentPath) { + protected GpuProgram getGpuProgram(GpuResourceCache cache, Object programKey, int shaderPath, int fragmentPath) { GpuProgram program = cache.getProgram(programKey); @@ -138,4 +170,14 @@ protected GpuProgram getGpuProgram(GpuResourceCache cache, Object programKey, St return program; } + + public void beginDrawing() { + GLES20.glDisable(GLES20.GL_DEPTH_TEST); + GLES20.glDepthMask(false); + } + + public void endDrawing() { + GLES20.glEnable(GLES20.GL_DEPTH_TEST); + GLES20.glDepthMask(true); + } } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/render/WWTexture.java b/WorldWindAndroid/src/gov/nasa/worldwind/render/WWTexture.java new file mode 100644 index 0000000..396c835 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/render/WWTexture.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ + +package gov.nasa.worldwind.render; + +import android.graphics.RectF; + +/** + * Represents a texture derived from an image source such as an image file or a {@link java.awt.image.BufferedImage}. + *

    + * The interface contains a method, {@link #isTextureInitializationFailed()} to determine whether the instance failed to + * convert an image source to a texture. If such a failure occurs, the method returns true and no further attempts are + * made to create the texture. + * + * @author tag + * @version $Id: WWTexture.java 1171 2013-02-11 21:45:02Z dcollins $ + */ +public interface WWTexture +{ + /** + * Returns the texture's image source. + * + * @return the texture's image source. + */ + public Object getImageSource(); + + /** + * Makes this texture the current texture for rendering. + *

    + * If the implementing instance's internal texture has not been created from its image source, the implementing + * class determines when the texture is retrieved and available. + *

    + * If a texture cannot be created from its image source it cannot be bound. This method returns an indication of + * whether the texture was bound or was not bound due to a failure during creation. + * + * @param dc the current draw context. + * + * @return true if the texture was bound, otherwise false. + */ + public boolean bind(DrawContext dc); + + /** + * Applies any necessary transformations to the texture prior to its being rendered. A common transformation is + * mapping texture coordinates from a flipped or non-square state to conventionally oriented OpenGL values. + * + * @param dc the current draw context. + * + * @throws IllegalArgumentException if the draw context is null. + */ + public void applyInternalTransform(DrawContext dc); + + /** + * Indicates whether the texture is currently available for use without regenerating it from its image source. + * + * @param dc the current draw context + * + * @return true if the texture is available and consistent with its image source, otherwise false. + * + * @throws IllegalArgumentException if the draw context is null. + */ + boolean isTextureCurrent(DrawContext dc); + + /** + * Returns the texture's width. + * + * @param dc the current draw context + * + * @return the texture's width, or 0 if the texture's size is currently unknown. + */ + int getWidth(DrawContext dc); + + /** + * Returns the texture's height + * + * @param dc the current draw context + * + * @return the texture's height, or 0 if the texture's size is currently unknown. + */ + int getHeight(DrawContext dc); + + /** + * Returns the texture's texture coordinates, which may be other than [0,0],[1,1] if the texture size is not a power + * of two or the texture must be flipped when rendered. + * + * @return returns the texture's texture coordinates. + */ + RectF getTexCoords(); + + /** + * Indicates whether an attempt to initialize the texture failed, which occurs when the image source is a + * non-existent image file or for other reasons specific to the image source. + * + * @return true if texture initialization failed, otherwise false. + */ + boolean isTextureInitializationFailed(); +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/retrieve/AbstractRetrievalPostProcessor.java b/WorldWindAndroid/src/gov/nasa/worldwind/retrieve/AbstractRetrievalPostProcessor.java index 20c53ea..7f80127 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/retrieve/AbstractRetrievalPostProcessor.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/retrieve/AbstractRetrievalPostProcessor.java @@ -7,11 +7,21 @@ package gov.nasa.worldwind.retrieve; import android.graphics.Bitmap; -import gov.nasa.worldwind.avlist.*; -import gov.nasa.worldwind.util.*; +import android.opengl.ETC1Util; +import gov.nasa.worldwind.WorldWindowImpl; +import gov.nasa.worldwind.avlist.AVKey; +import gov.nasa.worldwind.avlist.AVList; +import gov.nasa.worldwind.util.ImageUtil; +import gov.nasa.worldwind.util.Logging; +import gov.nasa.worldwind.util.WWIO; +import gov.nasa.worldwind.util.WWUtil; import gov.nasa.worldwind.util.dds.DDSCompressor; +import gov.nasa.worldwind.util.pkm.ETC1Compressor; -import java.io.*; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; import java.net.HttpURLConnection; import java.nio.ByteBuffer; import java.nio.channels.ClosedByInterruptException; @@ -528,6 +538,9 @@ protected ByteBuffer handleImageContent() throws IOException if (outFile.getPath().endsWith("dds")) return this.saveDDS(); + if(outFile.getPath().endsWith("pkm")) + return this.savePKM(); + Bitmap image = this.transformPixels(); if (image != null) @@ -643,4 +656,61 @@ protected ByteBuffer convertToDDS() throws IOException return buffer; } + + /** + * Saves a PKM image file after first converting any other image format to PKM. + * + * @return the converted image data if a conversion is performed, otherwise the original image data. + * + * @throws IOException if an IO error occurs while converting or saving the image. + */ + protected ByteBuffer savePKM() throws IOException + { + if(WorldWindowImpl.DEBUG) + Logging.verbose("Converting PKM file: " + getOutputFile().toString()); + + ByteBuffer buffer = this.getRetriever().getBuffer(); + try { + if (!this.getRetriever().getContentType().contains("pkm")) { + ETC1Util.ETC1Texture[] etc1 = this.convertToPKM(); + if (WorldWindowImpl.DEBUG) + Logging.verbose("Conversion finished. Saving PKM file: " + getOutputFile().toString()); + + FileOutputStream fis = new FileOutputStream(getOutputFile()); + ETC1Util.writeTexture(etc1[0], fis); + WWIO.closeStream(fis, getOutputFile().getName()); + + if(etc1.length>1) { + String path = getOutputFile().getPath(); + path = path.substring(0, path.lastIndexOf(".pkm"))+"_alpha.pkm"; + if (WorldWindowImpl.DEBUG) + Logging.verbose("Conversion finished. Saving PKM Alpha file: " + path); + fis = new FileOutputStream(path); + ETC1Util.writeTexture(etc1[1], fis); + WWIO.closeStream(fis, path); + } + } else { + this.saveBuffer(buffer); + } + } catch(Exception e) { + Logging.error("savePKM Exception", e); + } + return buffer; + } + + /** + * Converts an image to PKM. If the image format is not originally PKM, calls {@link #transformPixels()} to perform + * any defined image transform. + * + * @return the converted image data if a conversion is performed, otherwise the original image data. + * + * @throws IOException if an IO error occurs while converting the image. + */ + protected ETC1Util.ETC1Texture[] convertToPKM() throws IOException + { + Bitmap image = this.transformPixels(); + + return image!=null ? ETC1Compressor.compressImage(image) + : ETC1Compressor.compressImageBuffer(this.getRetriever().getBuffer()); + } } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/terrain/BasicElevationModel.java b/WorldWindAndroid/src/gov/nasa/worldwind/terrain/BasicElevationModel.java index e516f02..ec08ea3 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/terrain/BasicElevationModel.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/terrain/BasicElevationModel.java @@ -1040,7 +1040,7 @@ public void loadExtremeElevations(String extremesFileName) { // Look directly in the file system File file = new File(extremesFileName); if (file.exists()) is = new FileInputStream(file); - else Logging.warning("BasicElevationModel.UnavailableExtremesFile", extremesFileName); + else Logging.warning("UnavailExtremeFile", extremesFileName); } if (is == null) return; diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/terrain/SectorGeometry.java b/WorldWindAndroid/src/gov/nasa/worldwind/terrain/SectorGeometry.java index a7a69bd..41d634d 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/terrain/SectorGeometry.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/terrain/SectorGeometry.java @@ -40,9 +40,51 @@ public interface SectorGeometry void renderOutline(DrawContext dc); + /** + * Displays the geometry's bounding volume. + * + * @param dc the current draw context. + * + * @throws IllegalArgumentException if the draw context is null. + */ + void renderBoundingVolume(DrawContext dc); + + /** + * Displays on the geometry's surface the tessellator level and the minimum and maximum elevations of the sector. + * + * @param dc the current draw context. + * + * @throws IllegalArgumentException if the draw context is null. + */ + void renderTileID(DrawContext dc); + void beginRendering(DrawContext dc); void endRendering(DrawContext dc); void pick(DrawContext dc, Point pickPoint); + + /** + * Computes the Cartesian coordinates of a line's intersections with the geometry. + * + * @param line the line to intersect. + * + * @return the Cartesian coordinates of each intersection, or null if there is no intersection or no internal + * geometry has been computed. + * + * @throws IllegalArgumentException if the line is null. + */ + Intersection[] intersect(Line line); + + /** + * Computes the geometry's intersections with a globe at a specified elevation. + * + * @param elevation the elevation for which intersection points are to be found. + * + * @return an array of intersection pairs, or null if no intersections were found. The returned array of + * intersections describes a list of individual segments - two Intersection elements for each, + * corresponding to each geometry triangle that intersects the given elevation. + */ + Intersection[] intersect(double elevation); + } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/terrain/SectorGeometryList.java b/WorldWindAndroid/src/gov/nasa/worldwind/terrain/SectorGeometryList.java index 984245b..fc2097f 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/terrain/SectorGeometryList.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/terrain/SectorGeometryList.java @@ -39,4 +39,30 @@ public interface SectorGeometryList extends List void endRendering(DrawContext dc); void pick(DrawContext dc, Point pickPoint); + + /** + * Determines if and where a ray intersects the geometry. + * + * @param line the Line for which an intersection is to be found. + * + * @return the point closest to the ray origin where an intersection has been found or null if no + * intersection was found. + */ + public Intersection[] intersect(Line line); + + /** + * Determines if and where the geometry intersects the ellipsoid at a given elevation. + *

    + * The returned array of Intersection describes a list of individual segments - two + * Intersection for each, corresponding to each geometry triangle that intersects the given elevation. + *

    + * Note that the provided bounding Sector only serves as a 'hint' to avoid processing unnecessary + * geometry tiles. The returned intersection list may contain segments outside that sector. + * + * @param elevation the elevation for which intersections are to be found. + * @param sector the sector inside which intersections are to be found. + * + * @return a list of Intersection pairs/segments describing a contour line at the given elevation. + */ + public Intersection[] intersect(double elevation, Sector sector); } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/terrain/TiledTessellator.java b/WorldWindAndroid/src/gov/nasa/worldwind/terrain/TiledTessellator.java index 28bf102..529c862 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/terrain/TiledTessellator.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/terrain/TiledTessellator.java @@ -4,27 +4,20 @@ */ package gov.nasa.worldwind.terrain; -import gov.nasa.worldwind.Configuration; -import gov.nasa.worldwind.WWObjectImpl; -import gov.nasa.worldwind.WorldWind; +import android.graphics.*; +import gov.nasa.worldwind.*; import gov.nasa.worldwind.avlist.AVKey; import gov.nasa.worldwind.avlist.AVList; import gov.nasa.worldwind.cache.BasicMemoryCache; import gov.nasa.worldwind.cache.Cacheable; import gov.nasa.worldwind.cache.GpuResourceCache; import gov.nasa.worldwind.cache.MemoryCache; -import gov.nasa.worldwind.geom.Angle; -import gov.nasa.worldwind.geom.Extent; -import gov.nasa.worldwind.geom.Line; +import gov.nasa.worldwind.geom.*; import gov.nasa.worldwind.geom.Matrix; -import gov.nasa.worldwind.geom.Position; -import gov.nasa.worldwind.geom.Sector; -import gov.nasa.worldwind.geom.Vec4; import gov.nasa.worldwind.globes.Globe; import gov.nasa.worldwind.pick.PickedObject; +import gov.nasa.worldwind.render.*; import gov.nasa.worldwind.render.Color; -import gov.nasa.worldwind.render.DrawContext; -import gov.nasa.worldwind.render.GpuProgram; import gov.nasa.worldwind.util.BufferUtil; import gov.nasa.worldwind.util.Level; import gov.nasa.worldwind.util.LevelSet; @@ -35,24 +28,21 @@ import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.nio.ShortBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import javax.xml.xpath.XPath; import org.w3c.dom.Element; -import android.graphics.Point; import android.opengl.GLES20; import android.util.Pair; /** * Edited By: Nicola Dorigatti, Trilogis - * + * * @author dcollins * @version $Id: TiledTessellator.java 842 2012-10-09 23:46:47Z tgaskins $ */ -public class TiledTessellator extends WWObjectImpl implements Tessellator, Tile.TileFactory { +public class TiledTessellator extends WWObjectImpl + implements Tessellator, Tile.TileFactory { + protected static class TerrainTile extends Tile implements SectorGeometry { protected TiledTessellator tessellator; protected Extent extent; @@ -150,6 +140,27 @@ public void renderOutline(DrawContext dc) { this.tessellator.renderOutline(dc, this); } + public void renderBoundingVolume(DrawContext dc) + { + if (dc == null) { + String msg = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + this.tessellator.renderBoundingVolume(dc, this); + } + + public void renderTileID(DrawContext dc) + { + if (dc == null) { + String msg = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + this.tessellator.renderTileID(dc, this); + } + /** {@inheritDoc} */ public void beginRendering(DrawContext dc) { if (dc == null) { @@ -188,12 +199,21 @@ public void pick(DrawContext dc, Point pickPoint) { this.tessellator.pick(dc, this, pickPoint); } + + @Override + public Intersection[] intersect(Line line) + { + return this.tessellator.intersect(this, line); + } + + @Override + public Intersection[] intersect(double elevation) + { + return this.tessellator.intersect(this, elevation); + } } protected static class TerrainTileList extends ArrayList implements SectorGeometryList { - /** - * - */ private static final long serialVersionUID = 7740545209150322934L; protected Sector sector; protected TiledTessellator tessellator; @@ -272,6 +292,107 @@ public void pick(DrawContext dc, Point pickPoint) { this.tessellator.pick(dc, this, pickPoint); } + + /** + * Determines if and where a ray intersects the geometry. + * + * @param line the Line for which an intersection is to be found. + * + * @return the point closest to the ray origin where an intersection has been found or null if no + * intersection was found. + */ + public Intersection[] intersect(Line line) + { + if (line == null) + { + String msg = Logging.getMessage("nullValue.LineIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + ArrayList sglist = new ArrayList(this); + + Intersection[] hits; + ArrayList list = new ArrayList(); + for (SectorGeometry sg : sglist) + { + if (sg.getExtent().intersects(line)) + if ((hits = sg.intersect(line)) != null) + list.addAll(Arrays.asList(hits)); + } + + int numHits = list.size(); + if (numHits == 0) + return null; + + hits = new Intersection[numHits]; + list.toArray(hits); + + final Vec4 origin = line.getOrigin(); + Arrays.sort(hits, new Comparator() + { + public int compare(Intersection i1, Intersection i2) + { + if (i1 == null && i2 == null) + return 0; + if (i2 == null) + return -1; + if (i1 == null) + return 1; + + Vec4 v1 = i1.getIntersectionPoint(); + Vec4 v2 = i2.getIntersectionPoint(); + double d1 = origin.distanceTo3(v1); + double d2 = origin.distanceTo3(v2); + return Double.compare(d1, d2); + } + }); + return hits; + } + + /** + * Determines if and where the geometry intersects the ellipsoid at a given elevation. + *

    + * The returned array of Intersection describes a list of individual segments - two + * Intersection for each, corresponding to each geometry triangle that intersects the given elevation. + *

    + * Note that the provided bounding Sector only serves as a 'hint' to avoid processing unnecessary + * geometry tiles. The returned intersection list may contain segments outside that sector. + * + * @param elevation the elevation for which intersections are to be found. + * @param sector the sector inside which intersections are to be found. + * + * @return a list of Intersection pairs/segments describing a contour line at the given elevation. + */ + public Intersection[] intersect(double elevation, Sector sector) + { + if (sector == null) + { + String message = Logging.getMessage("nullValue.SectorIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + ArrayList sglist = new ArrayList(this); + + Intersection[] hits; + ArrayList list = new ArrayList(); + for (SectorGeometry sg : sglist) + { + if (sector.intersects(sg.getSector())) + if ((hits = sg.intersect(elevation)) != null) + list.addAll(Arrays.asList(hits)); + } + + int numHits = list.size(); + if (numHits == 0) + return null; + + hits = new Intersection[numHits]; + list.toArray(hits); + + return hits; + } } protected static class TerrainGeometry implements Cacheable { @@ -281,6 +402,7 @@ protected static class TerrainGeometry implements Cacheable { protected final Object vboCacheKey = new Object(); protected boolean mustRegnerateVbos; protected TerrainSharedGeometry sharedGeom; + protected double verticalExaggeration; public TerrainGeometry() { } @@ -328,13 +450,14 @@ public TerrainPickGeometry() { protected static final double DEFAULT_DETAIL_HINT_ORIGIN = 1.3; protected static Map sharedGeometry = new HashMap(); protected static Map pickGeometry = new HashMap(); - protected static final String PICK_VERTEX_SHADER_PATH = "shaders/TiledTessellatorPick.vert"; - protected static final String PICK_FRAGMENT_SHADER_PATH = "shaders/TiledTessellatorPick.frag"; + protected static final int PICK_VERTEX_SHADER_PATH = R.raw.vertex_color_vert ; + protected static final int PICK_FRAGMENT_SHADER_PATH = R.raw.vertex_color_frag; protected double detailHintOrigin = DEFAULT_DETAIL_HINT_ORIGIN; protected double detailHint; + protected Globe globe; protected LevelSet levels; - protected List topLevelTiles = new ArrayList(); + protected List topLevelTiles = new ArrayList(); protected TerrainTileList currentTiles = new TerrainTileList(this); protected Sector currentCoverage = new Sector(); // Data structures used to track when the elevation model changes. @@ -355,6 +478,7 @@ public TerrainPickGeometry() { protected Line pickRay = new Line(); protected Vec4 pickedTriPoint = new Vec4(); protected Position pickedTriPos = new Position(); + protected TextRenderer textRenderer; public TiledTessellator(AVList params) { if (params == null) { @@ -407,16 +531,279 @@ public void propertyChange(PropertyChangeEvent event) { // Listen to the Globe's elevation model for changes in elevation model sectors. We mark these sectors as // expired and regenerate geometry for tiles intersecting these sectors. // noinspection StringEquality - if (event != null && event.getPropertyName() == AVKey.ELEVATION_MODEL && event.getNewValue() instanceof Tile) { - Sector tileSector = ((Tile) event.getNewValue()).getSector(); - this.markSectorExpired(tileSector); + if (AVKey.ELEVATION_MODEL.equals(event.getPropertyName()) && event.getNewValue() instanceof Tile) + { + Logging.info("Marking all tile sector expired for ElevationModel change"); + Sector tileSector = ((Tile) event.getNewValue()).getSector(); + this.markSectorExpired(tileSector); + } + else if (AVKey.ELEVATION_MODEL.equals(event.getPropertyName()) && event.getNewValue() instanceof ElevationModel) + { + Logging.info("Marking all sectors expired for ElevationModel change"); + for(SectorGeometry tile : currentTiles) { + markSectorExpired(tile.getSector()); + } + } + else if(AVKey.VERTICAL_EXAGGERATION.equals(event.getPropertyName())) + { + Logging.info(String.format("Marking all sectors expired for VerticalExaggeration change: %.2f -> %.2f", + event.getOldValue(), event.getNewValue())); + for(SectorGeometry tile : currentTiles) + markSectorExpired(tile.getSector()); + } + } + + /** + * Determines if and where a ray intersects a TerrainTile geometry. + * + * @param tile the TerrainTile which geometry is to be tested for intersection. + * @param line the ray for which an intersection is to be found. + * + * @return an array of Intersection sorted by increasing distance from the line origin, or null if no + * intersection was found. + */ + protected Intersection[] intersect(TerrainTile tile, Line line) + { + if (line == null) + { + String msg = Logging.getMessage("nullValue.LineIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + TerrainGeometry geometry = tile.getGeometry(getTerrainGeometryCache()); + + if (geometry==null || geometry.points==null) + return null; + + // Compute 'vertical' plane perpendicular to the ground, that contains the ray + Vec4 normalV = new Vec4(); + globe.computeSurfaceNormalAtPoint(line.getOrigin(), normalV); + line.getDirection().cross3(normalV); + Plane verticalPlane = new Plane(normalV.x, normalV.y, normalV.z, -line.getOrigin().dot3(normalV)); + if (!tile.getExtent().intersects(verticalPlane)) + return null; + + // Compute 'horizontal' plane perpendicular to the vertical plane, that contains the ray + Vec4 normalH = line.getDirection().cross3(normalV); + Plane horizontalPlane = new Plane(normalH.x, normalH.y, normalH.z, -line.getOrigin().dot3(normalH)); + if (!tile.getExtent().intersects(horizontalPlane)) + return null; + + Intersection[] hits; + ArrayList list = new ArrayList(); + + short[] indices = new short[geometry.sharedGeom.indices.limit()]; + float[] coords = new float[geometry.points.limit()]; + geometry.sharedGeom.indices.rewind(); + geometry.points.rewind(); + geometry.sharedGeom.indices.get(indices, 0, indices.length); + geometry.points.get(coords, 0, coords.length); + geometry.sharedGeom.indices.rewind(); + geometry.points.rewind(); + + int trianglesNum = geometry.sharedGeom.indices.capacity() - 2; + double centerX = geometry.referenceCenter.x; + double centerY = geometry.referenceCenter.y; + double centerZ = geometry.referenceCenter.z; + + // Compute maximum cell size based on tile delta lat, density and globe radius + double effectiveRadiusVertical = tile.extent.getEffectiveRadius(verticalPlane); + double effectiveRadiusHorizontal = tile.extent.getEffectiveRadius(horizontalPlane); + + // Loop through all tile cells - triangle pairs + int startIndex = (tile.getWidth() + 2) * 2 + 6; // skip first skirt row and a couple degenerate cells + int endIndex = trianglesNum - startIndex; // ignore last skirt row and a couple degenerate cells + int k = -1; + for (int i = startIndex; i < endIndex; i += 2) + { + // Skip skirts and degenerate triangle cells - based on index sequence. + k = k == tile.getWidth() - 1 ? -4 : k + 1; // density x terrain cells interleaved with 4 skirt and degenerate cells. + if (k < 0) + continue; + + // Triangle pair diagonal - v1 & v2 + int vIndex = 3 * indices[i + 1]; + Vec4 v1 = new Vec4( + coords[vIndex++] + centerX, + coords[vIndex++] + centerY, + coords[vIndex] + centerZ); + + vIndex = 3 * indices[i + 2]; + Vec4 v2 = new Vec4( + coords[vIndex++] + centerX, + coords[vIndex++] + centerY, + coords[vIndex] + centerZ); + + Vec4 cellCenter = Vec4.mix3(.5, v1, v2); + + // Test cell center distance to vertical plane + if (Math.abs(verticalPlane.distanceTo(cellCenter)) > effectiveRadiusVertical) + continue; + + // Test cell center distance to horizontal plane + if (Math.abs(horizontalPlane.distanceTo(cellCenter)) > effectiveRadiusHorizontal) + continue; + + // Prepare to test triangles - get other two vertices v0 & v3 + Vec4 p = new Vec4(); + vIndex = 3 * indices[i]; + Vec4 v0 = new Vec4( + coords[vIndex++] + centerX, + coords[vIndex++] + centerY, + coords[vIndex] + centerZ); + + vIndex = 3 * indices[i + 3]; + Vec4 v3 = new Vec4( + coords[vIndex++] + centerX, + coords[vIndex++] + centerY, + coords[vIndex] + centerZ); + + // Test triangle 1 intersection w ray + Triangle t = new Triangle(v0, v1, v2); + if (t.intersect(line, p)) + { + list.add(new Intersection(p, false)); + } + + // Test triangle 2 intersection w ray + t = new Triangle(v1, v2, v3); + if (t.intersect(line, p)) + { + list.add(new Intersection(p, false)); + } + } + + int numHits = list.size(); + if (numHits == 0) + return null; + + hits = new Intersection[numHits]; + list.toArray(hits); + + final Vec4 origin = line.getOrigin(); + Arrays.sort(hits, new Comparator() + { + public int compare(Intersection i1, Intersection i2) + { + if (i1 == null && i2 == null) + return 0; + if (i2 == null) + return -1; + if (i1 == null) + return 1; + + Vec4 v1 = i1.getIntersectionPoint(); + Vec4 v2 = i2.getIntersectionPoint(); + double d1 = origin.distanceTo3(v1); + double d2 = origin.distanceTo3(v2); + return Double.compare(d1, d2); + } + }); + + return hits; + } + + protected Intersection[] intersect(TerrainTile tile, double elevation) + { + TerrainGeometry geometry = tile.getGeometry(getTerrainGeometryCache()); + + if (geometry==null || geometry.points==null) + return null; + + // Check whether the tile includes the intersection elevation - assume cylinder as Extent + // TODO: replace this test with a generic test against Extent + if (tile.getExtent() instanceof Cylinder) + { + Cylinder cylinder = ((Cylinder) tile.getExtent()); + if (!(globe.isPointAboveElevation(cylinder.getBottomCenter(), elevation) + ^ globe.isPointAboveElevation(cylinder.getTopCenter(), elevation))) + return null; + } + + Intersection[] hits; + ArrayList list = new ArrayList(); + + short[] indices = new short[geometry.sharedGeom.indices.limit()]; + float[] coords = new float[geometry.points.limit()]; + geometry.sharedGeom.indices.rewind(); + geometry.points.rewind(); + geometry.sharedGeom.indices.get(indices, 0, indices.length); + geometry.points.get(coords, 0, coords.length); + geometry.sharedGeom.indices.rewind(); + geometry.points.rewind(); + + int trianglesNum = geometry.sharedGeom.indices.capacity() - 2; + double centerX = geometry.referenceCenter.x; + double centerY = geometry.referenceCenter.y; + double centerZ = geometry.referenceCenter.z; + + // Loop through all tile cells - triangle pairs + int startIndex = (tile.getWidth() + 2) * 2 + 6; // skip first skirt row and a couple degenerate cells + int endIndex = trianglesNum - startIndex; // ignore last skirt row and a couple degenerate cells + int k = -1; + for (int i = startIndex; i < endIndex; i += 2) + { + // Skip skirts and degenerate triangle cells - based on indice sequence. + k = k == tile.getWidth() - 1 ? -4 : k + 1; // density x terrain cells interleaved with 4 skirt and degenerate cells. + if (k < 0) + continue; + + // Get the four cell corners + int vIndex = 3 * indices[i]; + Vec4 v0 = new Vec4( + coords[vIndex++] + centerX, + coords[vIndex++] + centerY, + coords[vIndex] + centerZ); + + vIndex = 3 * indices[i + 1]; + Vec4 v1 = new Vec4( + coords[vIndex++] + centerX, + coords[vIndex++] + centerY, + coords[vIndex] + centerZ); + + vIndex = 3 * indices[i + 2]; + Vec4 v2 = new Vec4( + coords[vIndex++] + centerX, + coords[vIndex++] + centerY, + coords[vIndex] + centerZ); + + vIndex = 3 * indices[i + 3]; + Vec4 v3 = new Vec4( + coords[vIndex++] + centerX, + coords[vIndex++] + centerY, + coords[vIndex] + centerZ); + + Intersection[] inter; + // Test triangle 1 intersection + if ((inter = globe.intersect(new Triangle(v0, v1, v2), elevation)) != null) + { + list.add(inter[0]); + list.add(inter[1]); + } + + // Test triangle 2 intersection + if ((inter = globe.intersect(new Triangle(v1, v2, v3), elevation)) != null) + { + list.add(inter[0]); + list.add(inter[1]); + } } + + int numHits = list.size(); + if (numHits == 0) + return null; + + hits = new Intersection[numHits]; + list.toArray(hits); + + return hits; } protected boolean getSurfacePoint(Angle latitude, Angle longitude, Vec4 result) { for (SectorGeometry tile : this.currentTiles) { if (tile.getSurfacePoint(latitude, longitude, result)) // Each tile tests the location against its sector. - return true; + return true; } return false; @@ -450,7 +837,7 @@ public SectorGeometryList tessellate(DrawContext dc) { return this.currentTiles; } - public Tile createTile(Sector sector, Level level, int row, int column) { + public TerrainTile createTile(Sector sector, Level level, int row, int column) { if (sector == null) { String msg = Logging.getMessage("nullValue.SectorIsNull"); Logging.error(msg); @@ -485,7 +872,7 @@ protected void assembleTiles(DrawContext dc) { this.currentTiles.clear(); this.currentCoverage.setDegrees(0, 0, 0, 0); - if (this.topLevelTiles.isEmpty()) this.createTopLevelTiles(); + if (this.topLevelTiles.isEmpty()) this.createTopLevelTiles(dc); for (int i = 0; i < this.topLevelTiles.size(); i++) { Tile tile = this.topLevelTiles.get(i); @@ -498,15 +885,24 @@ protected void assembleTiles(DrawContext dc) { this.currentTiles.setSector(this.currentCoverage.isEmpty() ? null : this.currentCoverage); } - protected void createTopLevelTiles() { + protected void createTopLevelTiles(DrawContext dc) { if (this.levels.getFirstLevel() == null) { Logging.warning(Logging.getMessage("generic.FirstLevelIsNull")); return; } + if(globe==null) { + this.globe = dc.getGlobe(); + dc.getGlobe().getElevationModel().addPropertyChangeListener(AVKey.ELEVATION_MODEL, this); + dc.addPropertyChangeListener(AVKey.VERTICAL_EXAGGERATION, this); + } + + Logging.verbose("Creating top level surface tiles"); + this.topLevelTiles.clear(); // TODO Tile.createTilesForLevel(this.levels.getFirstLevel(), this.levels.getSector(), this, this.topLevelTiles); Tile.createTilesForLevel(this.levels.getFirstLevel(), this.levels.getSector(), this, this.topLevelTiles, this.levels.getTileOrigin()); + Logging.verbose("Finished top level creation: #" + topLevelTiles.size()); } protected void addTileOrDescendants(DrawContext dc, TerrainTile tile) { @@ -591,8 +987,7 @@ protected Extent computeTileExtent(DrawContext dc, TerrainTile tile) { } protected boolean mustRegenerateGeometry(DrawContext dc, TerrainTile tile) { - MemoryCache cache = this.getTerrainGeometryCache(); - return tile.getGeometry(cache) == null || this.isExpired(dc, tile); + return this.isExpired(dc, tile); } protected void assembleExpiredSectors() { @@ -609,11 +1004,28 @@ protected void markSectorExpired(Sector sector) { } protected boolean isExpired(DrawContext dc, TerrainTile tile) { + TerrainGeometry geom = tile.getGeometry(getTerrainGeometryCache()); + if(geom==null) { +// if(WorldWindowImpl.DEBUG) +// Logging.verbose("Generating tile geometry for first time: " + tile); + return true; + } + if(geom.verticalExaggeration != dc.getVerticalExaggeration()) { + if(WorldWindowImpl.DEBUG) + Logging.verbose(String.format("Tile has expired due to Vertical Exaggeration. old: %.2f new: %.2f", + geom.verticalExaggeration, dc.getVerticalExaggeration())); + return true; + } + if (this.currentExpiredSectors.isEmpty()) return false; Sector tileSector = tile.getSector(); for (Sector sector : this.currentExpiredSectors) { - if (tileSector.intersects(sector)) return true; + if (tileSector.intersects(sector)) { + if(WorldWindowImpl.DEBUG) + Logging.verbose("Tile has expired due to expired sector: " + sector); + return true; + } } return false; @@ -626,14 +1038,16 @@ protected void regenerateGeometry(DrawContext dc, TerrainTile tile) { this.buildTileVertices(dc, tile, geom); this.buildSharedGeometry(tile, geom); - + geom.verticalExaggeration = dc.getVerticalExaggeration(); // Update the geometry's cached size. tile.setGeometry(cache, geom); + if(WorldWindowImpl.DEBUG) + Logging.verbose("added tile geometry: " + cache.getNumObjects()); } /** * Returns the memory cache used to cache terrain tiles, initializing the cache if it doesn't yet exist. - * + * * @return the memory cache associated with terrain tiles. */ protected MemoryCache getTerrainTileCache() { @@ -649,7 +1063,7 @@ protected MemoryCache getTerrainTileCache() { /** * Returns the memory cache used to cache terrain geometry, initializing the cache if it doesn't yet exist. - * + * * @return the memory cache associated with terrain geometry. */ protected MemoryCache getTerrainGeometryCache() { @@ -658,6 +1072,19 @@ protected MemoryCache getTerrainGeometryCache() { MemoryCache cache = new BasicMemoryCache((long) (0.8 * size), size); cache.setName("Tessellator Geometry"); WorldWind.getMemoryCacheSet().put(TerrainGeometry.class.getName(), cache); + if(WorldWindowImpl.DEBUG) { + cache.addCacheListener(new MemoryCache.CacheListener() { + @Override + public void entryRemoved(Object key, Object value) { + Logging.verbose("TerrainGeometryCache entry removed: " + key); + } + + @Override + public void removalException(Throwable exception, Object key, Object value) { + + } + }); + } } return WorldWind.getMemoryCacheSet().get(TerrainGeometry.class.getName()); @@ -868,16 +1295,16 @@ protected FloatBuffer buildTexCoords(int tileWidth, int tileHeight) { for (int j = 0; j < numLat; j++) { if (j <= 1) // First two columns repeat the min T-coordinate to provide a column for the skirt. - t = minT; + t = minT; else if (j >= numLat - 2) // Last two columns repeat the max T-coordinate to provide a column for the skirt. - t = maxT; + t = maxT; else t += deltaT; // Non-boundary latitudes are separated by the cell latitude delta. for (int i = 0; i < numLon; i++) { if (i <= 1) // First two rows repeat the min S-coordinate to provide a row for the skirt. - s = minS; + s = minS; else if (i >= numLon - 2) // Last two rows repeat the max S-coordinate to provide a row for the skirt. - s = maxS; + s = maxS; else s += deltaS; // Non-boundary longitudes are separated by the cell longitude delta. texCoords.put((float) s).put((float) t); @@ -1030,17 +1457,21 @@ protected void beginRendering(DrawContext dc) { // beginRendering is called for each tile. int location = program.getAttribLocation("vertexPoint"); if (location >= 0) GLES20.glEnableVertexAttribArray(location); + WorldWindowImpl.glCheckError("glEnableVertexAttribArray"); // Enable the program's vertexTexCoord attribute, if one exists. The data for this attribute is specified when // beginRendering is called for each tile. location = program.getAttribLocation("vertexTexCoord"); if (location >= 0) GLES20.glEnableVertexAttribArray(location); + WorldWindowImpl.glCheckError("glEnableVertexAttribArray"); } protected void endRendering(DrawContext dc) { // Restore the array and element array buffer bindings to 0. GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + WorldWindowImpl.glCheckError("glBindBuffer"); GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0); + WorldWindowImpl.glCheckError("glBindBuffer"); GpuProgram program = dc.getCurrentProgram(); if (program == null) return; // Message logged in beginRendering(DrawContext). @@ -1049,11 +1480,13 @@ protected void endRendering(DrawContext dc) { // beginRendering. int location = program.getAttribLocation("vertexPoint"); if (location >= 0) GLES20.glDisableVertexAttribArray(location); + WorldWindowImpl.glCheckError("glDisableVertexAttribArray"); // Disable the program's vertexTexCoord attribute, if one exists. This restores the program state modified in // beginRendering. location = program.getAttribLocation("vertexTexCoord"); if (location >= 0) GLES20.glDisableVertexAttribArray(location); + WorldWindowImpl.glCheckError("glDisableVertexAttribArray"); } protected void beginRendering(DrawContext dc, TerrainTile tile) { @@ -1082,7 +1515,9 @@ protected void beginRendering(DrawContext dc, TerrainTile tile) { int[] vboIds = (int[]) gpuCache.get(geom.vboCacheKey); if (vboIds != null) { GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vboIds[0]); + WorldWindowImpl.glCheckError("glBindBuffer"); GLES20.glVertexAttribPointer(location, 3, GLES20.GL_FLOAT, false, 0, 0); + WorldWindowImpl.glCheckError("glVertexAttribPointer"); } else { String msg = Logging.getMessage("Tessellator.SurfaceGeometryVBONotInGpuCache", tile, gpuCache.getUsedCapacity()); Logging.warning(msg); @@ -1096,7 +1531,9 @@ protected void beginRendering(DrawContext dc, TerrainTile tile) { int[] sharedVboIds = (int[]) gpuCache.get(geom.sharedGeom.vboCacheKey); if (sharedVboIds != null) { GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, sharedVboIds[0]); + WorldWindowImpl.glCheckError("glBindBuffer"); GLES20.glVertexAttribPointer(location, 2, GLES20.GL_FLOAT, false, 0, 0); + WorldWindowImpl.glCheckError("glVertexAttribPointer"); } else { String msg = Logging.getMessage("Tessellator.SharedGeometryVBONotInGpuCache", tile, gpuCache.getUsedCapacity()); Logging.warning(msg); @@ -1130,7 +1567,9 @@ protected void render(DrawContext dc, TerrainTile tile) { } GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, sharedVboIds[1]); + WorldWindowImpl.glCheckError("glBindBuffer"); GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP, geom.sharedGeom.indices.remaining(), GLES20.GL_UNSIGNED_SHORT, 0); + WorldWindowImpl.glCheckError("glDrawElements"); } protected void renderWireframe(DrawContext dc, TerrainTile tile) { @@ -1148,7 +1587,9 @@ protected void renderWireframe(DrawContext dc, TerrainTile tile) { } GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, sharedVboIds[2]); + WorldWindowImpl.glCheckError("glBindBuffer"); GLES20.glDrawElements(GLES20.GL_LINES, geom.sharedGeom.wireframeIndices.remaining(), GLES20.GL_UNSIGNED_SHORT, 0); + WorldWindowImpl.glCheckError("glDrawElements"); } protected void renderOutline(DrawContext dc, TerrainTile tile) { @@ -1166,7 +1607,43 @@ protected void renderOutline(DrawContext dc, TerrainTile tile) { } GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, sharedVboIds[3]); + WorldWindowImpl.glCheckError("glBindBuffer"); GLES20.glDrawElements(GLES20.GL_LINE_STRIP, geom.sharedGeom.outlineIndices.remaining(), GLES20.GL_UNSIGNED_SHORT, 0); + WorldWindowImpl.glCheckError("glDrawElements"); + } + + protected void renderBoundingVolume(DrawContext dc, TerrainTile tile) + { + Extent extent = tile.getExtent(); + if (extent == null) + return; + + if (extent instanceof Renderable) + ((Renderable) extent).render(dc); + } + + protected void renderTileID(DrawContext dc, TerrainTile tile) + { + if(textRenderer==null) { + Paint paint = new Paint(); + paint.setColor(android.graphics.Color.RED); + textRenderer = new TextRenderer(dc, paint); + } + + textRenderer.beginDrawing(); + + String tileLabel = Integer.toString(tile.getLevelNumber()); + double[] elevs = dc.getGlobe().getMinAndMaxElevations(tile.getSector()); + if (elevs != null) + tileLabel += ", " + (int) elevs[0] + "/" + (int) elevs[1]; + + LatLon ll = tile.getSector().getCentroid(); + Vec4 pt = new Vec4(); + dc.getView().project(dc.getGlobe().computePointFromPosition(ll.getLatitude(), ll.getLongitude(), + dc.getGlobe().getElevation(ll.getLatitude(), ll.getLongitude())), pt); + textRenderer.draw(tileLabel, (int) pt.x, (int) pt.y); + + textRenderer.endDrawing(); } protected void loadGeometryVbos(DrawContext dc, TerrainGeometry geom) { @@ -1178,12 +1655,15 @@ protected void loadGeometryVbos(DrawContext dc, TerrainGeometry geom) { if (vboIds == null) { vboIds = new int[1]; GLES20.glGenBuffers(1, vboIds, 0); + WorldWindowImpl.glCheckError("glGenBuffers"); } try { int sizeInBytes = 4 * geom.points.remaining(); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vboIds[0]); + WorldWindowImpl.glCheckError("glBindBuffer"); GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, sizeInBytes, geom.points, GLES20.GL_STREAM_DRAW); + WorldWindowImpl.glCheckError("glBufferData"); // Don't overwrite these VBOs if they're already in the cache. Doing so would cause the cache to delete // the existing VBO objects. Since we're reusing the same VBO ids, this would delete the VBO ids we're @@ -1194,6 +1674,7 @@ protected void loadGeometryVbos(DrawContext dc, TerrainGeometry geom) { } finally { // Restore the array buffer binding to 0. GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + WorldWindowImpl.glCheckError("glBindBuffer"); } } @@ -1204,34 +1685,45 @@ protected void loadSharedGeometryVBOs(DrawContext dc, TerrainSharedGeometry geom vboIds = new int[4]; GLES20.glGenBuffers(4, vboIds, 0); + WorldWindowImpl.glCheckError("glGenBuffers"); try { long totalSizeInBytes = 0; int sizeInBytes = 4 * geom.texCoords.remaining(); totalSizeInBytes += sizeInBytes; GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vboIds[0]); + WorldWindowImpl.glCheckError("glBindBuffer"); GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, sizeInBytes, geom.texCoords, GLES20.GL_STREAM_DRAW); + WorldWindowImpl.glCheckError("glBufferData"); sizeInBytes = 2 * geom.indices.remaining(); totalSizeInBytes += sizeInBytes; GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, vboIds[1]); + WorldWindowImpl.glCheckError("glBindBuffer"); GLES20.glBufferData(GLES20.GL_ELEMENT_ARRAY_BUFFER, sizeInBytes, geom.indices, GLES20.GL_STREAM_DRAW); + WorldWindowImpl.glCheckError("glBufferData"); sizeInBytes = 2 * geom.wireframeIndices.remaining(); totalSizeInBytes += sizeInBytes; GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, vboIds[2]); + WorldWindowImpl.glCheckError("glBindBuffer"); GLES20.glBufferData(GLES20.GL_ELEMENT_ARRAY_BUFFER, sizeInBytes, geom.wireframeIndices, GLES20.GL_STREAM_DRAW); + WorldWindowImpl.glCheckError("glBufferData"); sizeInBytes = 2 * geom.outlineIndices.remaining(); totalSizeInBytes += sizeInBytes; GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, vboIds[3]); + WorldWindowImpl.glCheckError("glBindBuffer"); GLES20.glBufferData(GLES20.GL_ELEMENT_ARRAY_BUFFER, sizeInBytes, geom.outlineIndices, GLES20.GL_STREAM_DRAW); + WorldWindowImpl.glCheckError("glBufferData"); cache.put(geom.vboCacheKey, vboIds, GpuResourceCache.VBO_BUFFERS, totalSizeInBytes); } finally { // Restore the array and element array buffer bindings to 0. GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + WorldWindowImpl.glCheckError("glBindBuffer"); GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0); + WorldWindowImpl.glCheckError("glBindBuffer"); } } @@ -1242,11 +1734,15 @@ protected void pick(DrawContext dc, SectorGeometryList sgList, Point pickPoint) program.bind(); dc.setCurrentProgram(program); + program.loadUniform1b("uUseVertexColors", true); + program.loadUniform1f("uOpacity", 1f); + program.loadUniformColor("uColor", Color.white()); try { SectorGeometry sg = this.getPickedGeometry(dc, sgList, pickPoint); if (sg != null) sg.pick(dc, pickPoint); } finally { GLES20.glUseProgram(0); + WorldWindowImpl.glCheckError("glUseProgram"); dc.setCurrentProgram(null); } } @@ -1295,8 +1791,11 @@ protected SectorGeometry getPickedGeometry(DrawContext dc, SectorGeometryList sg // ensure that the pick colors can be used to compute an index into the SectorGeometryList. if (i > 0) color = dc.getUniquePickColor(); - // TODO: Cull SectorGeometry against the pick frustum. SectorGeometry sg = sgList.get(i); + + if(!dc.getPickFrustums().intersectsAny(sg.getExtent())) + continue; + sg.beginRendering(dc); try { // Convert the pick color from a packed 32-bit RGB color int to a RGB color with separate components @@ -1306,6 +1805,7 @@ protected SectorGeometry getPickedGeometry(DrawContext dc, SectorGeometryList sg // the "vertexColor" attrib array is not enabled, this constant value is used instead. OpenGL // expands this 3-component RGB color to a 4-component RGBA color, where the alpha component is 1.0. GLES20.glVertexAttrib3f(location, (float) this.pickColor.r, (float) this.pickColor.g, (float) this.pickColor.b); + WorldWindowImpl.glCheckError("glVertexAttrib3f"); sg.render(dc); } finally { sg.endRendering(dc); @@ -1430,7 +1930,7 @@ protected void buildPickGeometry(DrawContext dc, int tileWidth, int tileHeight, pickGeom.points.put(corners, 9, 3); // Upper-right vertex. if (i != 0 || j != 0) // The first triangle's color is allocated before this loop. - color = dc.getUniquePickColor(); + color = dc.getUniquePickColor(); colors[0] = colors[3] = colors[6] = (byte) Color.getColorIntRed(color); colors[1] = colors[4] = colors[7] = (byte) Color.getColorIntGreen(color); colors[2] = colors[5] = colors[8] = (byte) Color.getColorIntBlue(color); @@ -1482,7 +1982,9 @@ protected void drawTrianglesInPickColors(DrawContext dc, TerrainPickGeometry geo int pointLocation = program.getAttribLocation("vertexPoint"); if (pointLocation >= 0) { GLES20.glEnableVertexAttribArray(pointLocation); + WorldWindowImpl.glCheckError("glEnableVertexAttribArray"); GLES20.glVertexAttribPointer(pointLocation, 3, GLES20.GL_FLOAT, false, 0, geom.points); + WorldWindowImpl.glCheckError("glVertexAttribPointer"); } // Enable and specify the data for the program's vertexPoint attribute, if one exists. We specify a 3-element @@ -1491,7 +1993,9 @@ protected void drawTrianglesInPickColors(DrawContext dc, TerrainPickGeometry geo int colorLocation = program.getAttribLocation("vertexColor"); if (colorLocation >= 0) { GLES20.glEnableVertexAttribArray(colorLocation); + WorldWindowImpl.glCheckError("glEnableVertexAttribArray"); GLES20.glVertexAttribPointer(colorLocation, 3, GLES20.GL_UNSIGNED_BYTE, true, 0, geom.colors); + WorldWindowImpl.glCheckError("glVertexAttribPointer"); } // Multiply the View's modelview-projection matrix by the tile's transform matrix to correctly transform tile @@ -1502,9 +2006,12 @@ protected void drawTrianglesInPickColors(DrawContext dc, TerrainPickGeometry geo program.loadUniformMatrix("mvpMatrix", this.mvpMatrix); GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, geom.vertexCount); + WorldWindowImpl.glCheckError("glDrawArrays"); if (pointLocation >= 0) GLES20.glDisableVertexAttribArray(pointLocation); + WorldWindowImpl.glCheckError("glDisableVertexAttribArray"); if (colorLocation >= 0) GLES20.glDisableVertexAttribArray(colorLocation); + WorldWindowImpl.glCheckError("glDisableVertexAttribArray"); } protected PickedObject resolvePick(DrawContext dc, TerrainPickGeometry geom, Point pickPoint) { @@ -1600,7 +2107,11 @@ protected void computeSurfacePoint(TerrainTile tile, TerrainGeometry geom, Angle // upper-right. The cell's diagonal starts at the lower-left vertex and ends at the upper-right vertex. double sf = (s < tileWidth ? s - (int) s : 1); double tf = (t < tileHeight ? t - (int) t : 1); - this.computePointInCell(sf, tf, points[0], points[1], points[2], points[3], points[4], points[5], points[6], points[7], points[8], points[9], points[10], points[11], + this.computePointInCell(sf, tf, + points[0], points[1], points[2], + points[3], points[4], points[5], + points[6], points[7], points[8], + points[9], points[10], points[11], result); // Add the tile geometry's reference center to the result in order to transform it from tile local coordinates @@ -1611,7 +2122,7 @@ protected void computeSurfacePoint(TerrainTile tile, TerrainGeometry geom, Angle /** * Computes the model coordinate intersection of a specified line with a triangle specified by individual * coordinates. - * + * * @param line * the line to test. * @param vax @@ -1665,7 +2176,7 @@ protected boolean computeTriangleIntersection(Line line, double vax, double vay, // Compute dot product of N and ray direction. double b = nx * dir.x + ny * dir.y + nz * dir.z; if (b > -EPSILON && b < EPSILON) // ray is parallel to triangle plane - return false; + return false; double t = -(nx * tvecx + ny * tvecy + nz * tvecz) / b; line.getPointAt(t, result); @@ -1676,7 +2187,7 @@ protected boolean computeTriangleIntersection(Line line, double vax, double vay, /** * Computes the point in tile local coordinates of a location within a tile's cell specified by individual * coordinates. - * + * * @param s * a parameterized horizontal coordinate within the tile's 2D grid of points as a floating-point value * in the range [0, tileWidth]. @@ -1711,7 +2222,7 @@ protected boolean computeTriangleIntersection(Line line, double vax, double vay, * contains the tile local coordinates of the point in the cell after this method returns. */ protected void computePointInCell(double s, double t, double llx, double lly, double llz, double lrx, double lry, double lrz, double ulx, double uly, double ulz, double urx, - double ury, double urz, Vec4 result) { + double ury, double urz, Vec4 result) { if (s < t) // The point is in the lower-right triangle. { double oneMinusS = 1 - s; @@ -1726,4 +2237,82 @@ protected void computePointInCell(double s, double t, double llx, double lly, do result.z = ulz + s * (urz - ulz) + oneMinusT * (llz - ulz); } } + + /** + * Computes the point in tile local coordinates of a location within a tile's cell specified by individual + * coordinates. + * + * @param s + * a parameterized horizontal coordinate within the tile's 2D grid of points as a floating-point value + * in the range [0, 1]. + * @param t + * a parameterized vertical coordinate within the tile's 2D grid of points as a floating-point value + * in the range [0, 1]. + * @param llx + * the X coordinate of the cell's lower left corner. + * @param lly + * the Y coordinate of the cell's lower left corner. + * @param llz + * the Z coordinate of the cell's lower left corner. + * @param lrx + * the X coordinate of the cell's lower right corner. + * @param lry + * the Y coordinate of the cell's lower right corner. + * @param lrz + * the Z coordinate of the cell's lower right corner. + * @param ulx + * the X coordinate of the cell's upper left corner. + * @param uly + * the Y coordinate of the cell's upper left corner. + * @param ulz + * the Z coordinate of the cell's upper left corner. + * @param urx + * the X coordinate of the cell's upper right corner. + * @param ury + * the Y coordinate of the cell's upper right corner. + * @param urz + * the Z coordinate of the cell's upper right corner. + * @param result + * contains the tile local coordinates of the point in the cell after this method returns. + */ + protected void computePointInCellBarycentric(double s, double t, double llx, double lly, double llz, double lrx, double lry, double lrz, double ulx, double uly, double ulz, double urx, + double ury, double urz, Vec4 result) { + Vec4 barycentric = new Vec4(); + if (s < t) // The point is in the lower-right triangle. + { + barycentric2D(LOWER_RIGHT, UPPER_RIGHT, LOWER_LEFT, new Vec4(s, t), barycentric); + result.x = lrx*barycentric.x + urx*barycentric.y + llx*barycentric.z; + result.y = lry*barycentric.x + ury*barycentric.y + lly*barycentric.z; + result.z = lrz*barycentric.x + urz*barycentric.y + llz*barycentric.z; + } else // The point is in the upper-left triangle, or on the diagonal between the two triangles. + { + barycentric2D(UPPER_LEFT, LOWER_LEFT, UPPER_RIGHT, new Vec4(s, t), barycentric); + result.x = ulx*barycentric.x + llx*barycentric.y + urx*barycentric.z; + result.y = uly*barycentric.x + lly*barycentric.y + ury*barycentric.z; + result.z = ulz*barycentric.x + llz*barycentric.y + urz*barycentric.z; + } + } + + private final Vec4 LOWER_LEFT = new Vec4(0, 0); + private final Vec4 LOWER_RIGHT = new Vec4(1, 0); + private final Vec4 UPPER_RIGHT = new Vec4(1, 1); + private final Vec4 UPPER_LEFT = new Vec4(0, 1); + + /** + * Get barycentric coordinate from 2D coordinate in triangle a,b,c + * @param a First point in triangle + * @param b Second point in Triangle + * @param c Third point in triangle + * @param p point in triangle + * @param result barycentric coordinate (α, β, γ) for p + */ + public void barycentric2D(Vec4 a, Vec4 b, Vec4 c, Vec4 p, Vec4 result) { + double A = (b.x-a.x)*(c.y-a.y)-(c.x-a.x)*(b.y-a.y); + double Ab = (a.x-c.x)*(p.y-c.y)-(p.x-c.x)*(a.y-c.y); + double Ac = (b.x-a.x)*(p.y-a.y)-(p.x-a.x)*(b.y-a.y); + double β = Ab / A; + double γ = Ac / A; + double α = 1 - β - γ; + result.set(α, β, γ); + } } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/terrain/VisibleTerrain.java b/WorldWindAndroid/src/gov/nasa/worldwind/terrain/VisibleTerrain.java index c8fabea..134fdae 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/terrain/VisibleTerrain.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/terrain/VisibleTerrain.java @@ -24,19 +24,19 @@ public VisibleTerrain(DrawContext dc) this.dc = dc; } - /** {@inheritDoc} */ + @Override public Globe getGlobe() { return this.dc.getGlobe(); } - /** {@inheritDoc} */ + @Override public double getVerticalExaggeration() { return this.dc.getVerticalExaggeration(); } - /** {@inheritDoc} */ + @Override public Double getElevation(Angle latitude, Angle longitude) { if (latitude == null) @@ -62,7 +62,7 @@ public Double getElevation(Angle latitude, Angle longitude) return p.distanceTo3(pt); } - /** {@inheritDoc} */ + @Override public Vec4 getSurfacePoint(Position position) { if (position == null) @@ -77,7 +77,7 @@ public Vec4 getSurfacePoint(Position position) return result; } - /** {@inheritDoc} */ + @Override public Vec4 getSurfacePoint(Angle latitude, Angle longitude, double metersOffset) { if (latitude == null) @@ -99,7 +99,7 @@ public Vec4 getSurfacePoint(Angle latitude, Angle longitude, double metersOffset return result; } - /** {@inheritDoc} */ + @Override public void getSurfacePoint(Position position, Vec4 result) { if (position == null) @@ -119,7 +119,7 @@ public void getSurfacePoint(Position position, Vec4 result) this.getSurfacePoint(position.latitude, position.longitude, position.elevation, result); } - /** {@inheritDoc} */ + @Override public void getSurfacePoint(Angle latitude, Angle longitude, double metersOffset, Vec4 result) { if (latitude == null) @@ -146,11 +146,13 @@ public void getSurfacePoint(Angle latitude, Angle longitude, double metersOffset SectorGeometryList sectorGeometry = this.dc.getSurfaceGeometry(); if (sectorGeometry != null && sectorGeometry.getSurfacePoint(latitude, longitude, result)) { - // The sector geometry already has vertical exaggeration applied. This has the effect of interpreting - // metersOffset as height above the terrain after vertical exaggeration is applied. - this.getGlobe().computeSurfaceNormalAtPoint(result, this.point); - this.point.multiply3AndSet(metersOffset); - result.add3AndSet(this.point); + if(metersOffset>0) { + // The sector geometry already has vertical exaggeration applied. This has the effect of interpreting + // metersOffset as height above the terrain after vertical exaggeration is applied. + this.getGlobe().computeSurfaceNormalAtPoint(result, this.point); + this.point.multiply3AndSet(metersOffset); + result.add3AndSet(this.point); + } } else { @@ -163,7 +165,7 @@ public void getSurfacePoint(Angle latitude, Angle longitude, double metersOffset } } - /** {@inheritDoc} */ + @Override public void getPoint(Position position, String altitudeMode, Vec4 result) { if (position == null) @@ -183,7 +185,7 @@ public void getPoint(Position position, String altitudeMode, Vec4 result) this.getPoint(position.latitude, position.longitude, position.elevation, altitudeMode, result); } - /** {@inheritDoc} */ + @Override public void getPoint(Angle latitude, Angle longitude, double metersOffset, String altitudeMode, Vec4 result) { if (latitude == null) diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/util/BufferUtil.java b/WorldWindAndroid/src/gov/nasa/worldwind/util/BufferUtil.java index fe660c0..b0e6f6e 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/util/BufferUtil.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/util/BufferUtil.java @@ -70,7 +70,30 @@ public static ShortBuffer newShortBuffer(int capacity) throw new IllegalArgumentException(msg); } - return ByteBuffer.allocateDirect(Short.SIZE / 8 * capacity).order(ByteOrder.nativeOrder()).asShortBuffer(); + return newByteBuffer(Short.SIZE / 8 * capacity).asShortBuffer(); + } + + /** + * Allocates and returns a new int buffer with the specified capacity, in number of int elements. The returned + * buffer is backed by a direct byte buffer who's byte order is set to the current platform byte order. See the + * section above on Creating Vertex Attribute Buffers for more information. + * + * @param capacity the new buffer's capacity, in number of int elements. + * + * @return a new int buffer with the specified capacity. + * + * @throws IllegalArgumentException if the capacity is less than 0. + */ + public static IntBuffer newIntBuffer(int capacity) + { + if (capacity < 0) + { + String msg = Logging.getMessage("generic.CapacityIsInvalid", capacity); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + return newByteBuffer(Integer.SIZE / 8 * capacity).asIntBuffer(); } /** @@ -93,6 +116,6 @@ public static FloatBuffer newFloatBuffer(int capacity) throw new IllegalArgumentException(msg); } - return ByteBuffer.allocateDirect(Float.SIZE / 8 * capacity).order(ByteOrder.nativeOrder()).asFloatBuffer(); + return newByteBuffer(Float.SIZE / 8 * capacity).asFloatBuffer(); } } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/util/GLRuntimeCapabilities.java b/WorldWindAndroid/src/gov/nasa/worldwind/util/GLRuntimeCapabilities.java new file mode 100644 index 0000000..e0a1e50 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/util/GLRuntimeCapabilities.java @@ -0,0 +1,442 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.util; + + +import java.util.Arrays; +import java.util.List; + +import static android.opengl.GLES20.*; + +/** + * GLRuntimeCapabilities describes the GL capabilities supported by the current GL runtime. It provides the caller with + * the current GL version, with information about which GL features are available, and with properties defining the + * capabilities of those features. + *

    + * For each GL feature, there are three key pieces of information available through GLRuntimeCapabilities:

    • The + * property is[Feature]Available defines whether or not the feature is supported by the current GL runtime. + * This is an attribute of the GL runtime, and is typically configured automatically by a call to {@link + * #initialize()}.
    • The property is[Feature]Enabled defines whether or + * not this feature should be used, and must be configured by the caller.
    • The convenience method + * isUse[Feature](). This returns whether or not the feature is available and is enabled for use (it is + * simply a conjunction of the "available" and "enabled" properties).
    + *

    + * GLRuntimeCapabilities is designed to automatically configure itself with information about the current GL runtime. To + * invoke this behavior, call {@link #initialize} with a valid GLContext at the beginning + * of each rendering pass. + * + * @author dcollins + * @version $Id: GLRuntimeCapabilities.java 1933 2014-04-14 22:54:19Z dcollins $ + */ +public class GLRuntimeCapabilities +{ + int[] mParam = new int[1]; + + protected String glVendor; + protected String glRenderer; + protected double glVersion; + protected int mGLES_Major_Version; + protected int mGLES_Minor_Version; + protected double glslVersion; + protected int mGLSL_Major_Version; + protected int mGLSL_Minor_Version; + + protected boolean isAnisotropicTextureFilterAvailable; + protected boolean isAnisotropicTextureFilterEnabled=true; + protected boolean isFramebufferObjectAvailable; + protected boolean isFramebufferObjectEnabled=true; + protected boolean isVertexBufferObjectAvailable; + protected boolean isVertexBufferObjectEnabled=true; + + protected double mMaxTextureAnisotropy=-1d; + protected int mDepthBits; + private int mMaxTextureSize; + private int mMaxCombinedTextureImageUnits; + private int mMaxCubeMapTextureSize; + private int mMaxFragmentUniformVectors; + private int mMaxRenderbufferSize; + private int mMaxTextureImageUnits; + private int mMaxVaryingVectors; + private int mMaxVertexAttribs; + private int mMaxVertexTextureImageUnits; + private int mMaxVertexUniformVectors; + private int mMaxViewportWidth; + private int mMaxViewportHeight; + private int mMinAliasedLineWidth; + private int mMaxAliasedLineWidth; + private int mMinAliasedPointSize; + private int mMaxAliasedPointSize; + + protected List mExtensions; + + private static GLRuntimeCapabilities instance; + + public static GLRuntimeCapabilities getInstance() { + if (instance == null) { + synchronized (GLRuntimeCapabilities.class) { + if(instance==null) { + instance = new GLRuntimeCapabilities(); + } + } + } + return instance; + } + + /** + * Constructs a new GLAtttributes, enabling framebuffer objects, anisotropic texture filtering, and vertex buffer + * objects. Note that these properties are marked as enabled, but they are not known to be available yet. All other + * properties are set to default values which may be set explicitly by the caller. + *

    + * Note: The default vertex-buffer usage flag can be set via {@link gov.nasa.worldwind.Configuration} using the key + * "gov.nasa.worldwind.avkey.VBOUsage". If that key is not specified in the configuration then vertex-buffer usage + * defaults to true. + */ + private GLRuntimeCapabilities() { + mParam = new int[1]; + + String[] versionString = (glGetString(GL_VERSION)).split(" "); + if (versionString.length >= 3) { + String[] versionParts = versionString[2].split("\\."); + if (versionParts.length >= 2) { + mGLES_Major_Version = Integer.parseInt(versionParts[0]); + if (versionParts[1].endsWith(":") || versionParts[1].endsWith("-")) { + versionParts[1] = versionParts[1].substring(0, versionParts[1].length() - 1); + } + mGLES_Minor_Version = Integer.parseInt(versionParts[1]); + glVersion = Double.parseDouble(mGLES_Major_Version + "." + mGLES_Minor_Version); + } + } + versionString = (glGetString(GL_SHADING_LANGUAGE_VERSION)).split(" "); + if (versionString.length >= 3) { + String[] versionParts = versionString[2].split("\\."); + if (versionParts.length >= 2) { + mGLSL_Major_Version = Integer.parseInt(versionParts[0]); + if (versionParts[1].endsWith(":") || versionParts[1].endsWith("-")) { + versionParts[1] = versionParts[1].substring(0, versionParts[1].length() - 1); + } + mGLSL_Minor_Version = Integer.parseInt(versionParts[1]); + glslVersion = Double.parseDouble(mGLSL_Major_Version + "." + mGLSL_Minor_Version); + } + } + + // Determine whether or not the OpenGL implementation is provided by the VMware SVGA 3D driver. This flag is + // used to work around bugs and unusual behavior in the VMware SVGA 3D driver. The VMware drivers tested on + // 7 August 2013 report the following strings for GL_VENDOR and GL_RENDERER: + // - GL_VENDOR: "VMware, Inc." + // - GL_RENDERER: "Gallium 0.4 on SVGA3D; build: RELEASE;" + glVendor = glGetString(GL_VENDOR); + glRenderer = glGetString(GL_RENDERER); + + mExtensions = Arrays.asList(glGetString(GL_EXTENSIONS).split(" ")); + + isAnisotropicTextureFilterAvailable = isExtensionAvailable("GL_EXT_texture_filter_anisotropic"); + isFramebufferObjectAvailable = true;//isExtensionAvailable(GL_EXT_framebuffer_object) || isExtensionAvailable("OES_fbo_render_mipmap"); + // Vertex Buffer Objects are supported in version 1.5 or greater only. we are using 2.0 + isVertexBufferObjectAvailable = true; + + // Documentation on the anisotropic texture filter is available at + // http://www.opengl.org/registry/specs/EXT/texture_filter_anisotropic.txt + if (isAnisotropicTextureFilterAvailable) + { + // The maxAnisotropy value can be any real value. A value less than 2.0 indicates that the graphics + // context does not support texture anisotropy. +// float[] params = new float[1]; +// glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, params, 0); +// this.maxTextureAnisotropy = params[0]; + } + mDepthBits = getInt(GL_DEPTH_BITS); + mMaxCombinedTextureImageUnits = getInt(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS); + mMaxCubeMapTextureSize = getInt(GL_MAX_CUBE_MAP_TEXTURE_SIZE); + mMaxFragmentUniformVectors = getInt(GL_MAX_FRAGMENT_UNIFORM_VECTORS); + mMaxRenderbufferSize = getInt(GL_MAX_RENDERBUFFER_SIZE); + mMaxTextureImageUnits = getInt(GL_MAX_TEXTURE_IMAGE_UNITS); + mMaxTextureSize = getInt(GL_MAX_TEXTURE_SIZE); + mMaxVaryingVectors = getInt(GL_MAX_VARYING_VECTORS); + mMaxVertexAttribs = getInt(GL_MAX_VERTEX_ATTRIBS); + mMaxVertexTextureImageUnits = getInt(GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS); + mMaxVertexUniformVectors = getInt(GL_MAX_VERTEX_UNIFORM_VECTORS); + mMaxViewportWidth = getInt(GL_MAX_VIEWPORT_DIMS, 2, 0); + mMaxViewportHeight = getInt(GL_MAX_VIEWPORT_DIMS, 2, 1); + mMinAliasedLineWidth = getInt(GL_ALIASED_LINE_WIDTH_RANGE, 2, 0); + mMaxAliasedLineWidth = getInt(GL_ALIASED_LINE_WIDTH_RANGE, 2, 1); + mMinAliasedPointSize = getInt(GL_ALIASED_POINT_SIZE_RANGE, 2, 0); + mMaxAliasedPointSize = getInt(GL_ALIASED_POINT_SIZE_RANGE, 2, 1); + } + + private int getInt(int pname) + { + glGetIntegerv(pname, mParam, 0); + return mParam[0]; + } + + private int getInt(int pname, int length, int index) + { + int[] params = new int[length]; + glGetIntegerv(pname, params, 0); + return params[index]; + } + + public boolean isExtensionAvailable(CharSequence extension) { + return this.mExtensions.contains(extension); + } + + /** + * Returns the current GL runtime version as a real number. For example, if the GL version is 1.5, this returns the + * floating point number equivalent to 1.5. + * + * @return GL version as a number. + */ + public double getGLVersion() + { + return this.glVersion; + } + + public double getGLSLVersion() + { + return this.glslVersion; + } + + /** + * Returns true if anisotropic texture filtering is available in the current GL runtime, and is enabled. Otherwise + * this returns false. For details on GL anisotropic texture filtering, see http://www.opengl.org/registry/specs/EXT/texture_filter_anisotropic.txt. + * + * @return true if anisotropic texture filtering is available and enabled, and false otherwise. + */ + public boolean isUseAnisotropicTextureFilter() + { + return this.isAnisotropicTextureFilterAvailable && this.isAnisotropicTextureFilterEnabled; + } + + /** + * Returns true if framebuffer objects are available in the current GL runtime, and are enabled. Otherwise this + * returns false. For details on GL framebuffer objects, see http://www.opengl.org/registry/specs/EXT/framebuffer_object.txt. + * + * @return true if framebuffer objects are available and enabled, and false otherwise. + */ + public boolean isUseFramebufferObject() + { + return this.isFramebufferObjectAvailable && this.isFramebufferObjectEnabled; + } + + public void setAnisotropicTextureFilterEnabled(boolean enable) + { + this.isAnisotropicTextureFilterEnabled = enable; + } + + /** + * Sets whether or not framebuffer objects should be used if they are available in the current GL runtime. + * + * @param enable true to enable framebuffer objects, false to disable them. + */ + public void setFramebufferObjectEnabled(boolean enable) + { + this.isFramebufferObjectEnabled = enable; + } + + /** + * Returns the number of bitplanes in the current GL depth buffer. The number of bitplanes is directly proportional + * to the accuracy of the GL renderer's hidden surface removal. The returned value is typically 16, 24 or 32. For + * more information on OpenGL depth buffering, see http://www.opengl.org/archives/resources/faq/technical/depthbuffer.htm. + * + * @return the number of bitplanes in the current GL depth buffer. + */ + public int getDepthBits() + { + return mDepthBits; + } + + /** + * Returns a real number defining the maximum degree of texture anisotropy supported by the current GL runtime. This + * defines the maximum ratio of the anisotropic texture filter. So 2.0 would define a maximum ratio of 2:1. If the + * degree is less than 2, then the anisotropic texture filter is not supported by the current GL runtime. + * + * @return the maximum degree of texture anisotropy supported. + */ + public double getMaxTextureAnisotropy() { + return mMaxTextureAnisotropy; + } + + /** + * A rough estimate of the largest texture that OpenGL can handle. + * @return + */ + public int getMaxTextureSize() + { + return mMaxTextureSize; + } + + /** + * The maximum supported texture image units that can be used to access texture maps from the vertex shader + * and the fragment processor combined. If both the vertex shader and the fragment processing stage access + * the same texture image unit, then that counts as using two texture image units against this limit. + * @return + */ + public int getMaxCombinedTextureUnits() + { + return mMaxCombinedTextureImageUnits; + } + + /** + * The value gives a rough estimate of the largest cube-map texture that the GL can handle. + * The value must be at least 1024. + * @return + */ + public int getMaxCubeMapTextureSize() + { + return mMaxCubeMapTextureSize; + } + + /** + * The maximum number of individual 4-vectors of floating-point, integer, or boolean values that can be held + * in uniform variable storage for a fragment shader. + * @return + */ + public int getMaxFragmentUniformVectors() + { + return mMaxFragmentUniformVectors; + } + + /** + * Indicates the maximum supported size for renderbuffers. + * @return + */ + public int getMaxRenderbufferSize() + { + return mMaxRenderbufferSize; + } + + /** + * The maximum supported texture image units that can be used to access texture maps from the fragment shader. + * @return + */ + public int getMaxTextureImageUnits() + { + return mMaxTextureImageUnits; + } + + /** + * The maximum number of 4-vectors for varying variables. + * @return + */ + public int getMaxVaryingVectors() + { + return mMaxVaryingVectors; + } + + /** + * The maximum number of 4-component generic vertex attributes accessible to a vertex shader. + * @return + */ + public int getMaxVertexAttribs() + { + return mMaxVertexAttribs; + } + + /** + * The maximum supported texture image units that can be used to access texture maps from the vertex shader. + * @return + */ + public int getMaxVertexTextureImageUnits() + { + return mMaxVertexTextureImageUnits; + } + + /** + * The maximum number of 4-vectors that may be held in uniform variable storage for the vertex shader. + * @return + */ + public int getMaxVertexUniformVectors() + { + return mMaxVertexUniformVectors; + } + + /** + * The maximum supported viewport width + * @return + */ + public int getMaxViewportWidth() + { + return mMaxViewportWidth; + } + + /** + * The maximum supported viewport height + * @return + */ + public int getMaxViewportHeight() + { + return mMaxViewportHeight; + } + + /** + * Indicates the minimum width supported for aliased lines + * @return + */ + public int getMinAliasedLineWidth() + { + return mMinAliasedLineWidth; + } + + /** + * Indicates the maximum width supported for aliased lines + * @return + */ + public int getMaxAliasedLineWidth() + { + return mMaxAliasedLineWidth; + } + + /** + * Indicates the minimum size supported for aliased points + * @return + */ + public int getMinAliasedPointSize() + { + return mMinAliasedPointSize; + } + + /** + * Indicates the maximum size supported for aliased points + * @return + */ + public int getMaxAliasedPointSize() + { + return mMaxAliasedPointSize; + } + + public String toString() + { + StringBuffer sb = new StringBuffer(); + sb.append("GL v%d.%d", mGLES_Major_Version, mGLES_Minor_Version); + sb.append("GLSL v%d.%d", mGLSL_Major_Version, mGLSL_Minor_Version); + sb.append("-=-=-=- OpenGL Capabilities -=-=-=-\n"); + sb.append("Max Cube Map Texture Size : ").append(mMaxCubeMapTextureSize).append("\n"); + sb.append("Max Fragment Uniform Vectors : ").append(mMaxFragmentUniformVectors).append("\n"); + sb.append("Max Renderbuffer Size : ").append(mMaxRenderbufferSize).append("\n"); + sb.append("Max Texture Image Units : ").append(mMaxTextureImageUnits).append("\n"); + sb.append("Max Texture Size : ").append(mMaxTextureSize).append("\n"); + sb.append("Max Varying Vectors : ").append(mMaxVaryingVectors).append("\n"); + sb.append("Max Vertex Attribs : ").append(mMaxVertexAttribs).append("\n"); + sb.append("Max Vertex Texture Image Units : ").append(mMaxVertexTextureImageUnits).append("\n"); + sb.append("Max Vertex Uniform Vectors : ").append(mMaxVertexUniformVectors).append("\n"); + sb.append("Max Viewport Width : ").append(mMaxViewportWidth).append("\n"); + sb.append("Max Viewport Height : ").append(mMaxViewportHeight).append("\n"); + sb.append("Min Aliased Line Width : ").append(mMinAliasedLineWidth).append("\n"); + sb.append("Max Aliased Line Width : ").append(mMaxAliasedLineWidth).append("\n"); + sb.append("Min Aliased Point Size : ").append(mMinAliasedPointSize).append("\n"); + sb.append("Max Aliased Point Width : ").append(mMaxAliasedPointSize).append("\n"); + sb.append("Depth Bits : ").append(mDepthBits).append("\n"); + sb.append("Is Anisotropic Texture Filter Avail: ").append(isAnisotropicTextureFilterAvailable).append("\n"); + sb.append("Max Texture Anisotropy : ").append(mMaxTextureAnisotropy).append("\n"); + sb.append("-=-=-=- /OpenGL Capabilities -=-=-=-\n"); + sb.append("-=-=-=- OpenGL Extensions -=-=-=-\n"); + for(String extension : mExtensions) + sb.append(extension).append("\n"); + sb.append("-=-=-=- /OpenGL Extensions -=-=-=-\n"); + return sb.toString(); + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/util/GLTextureView.java b/WorldWindAndroid/src/gov/nasa/worldwind/util/GLTextureView.java new file mode 100644 index 0000000..57234f8 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/util/GLTextureView.java @@ -0,0 +1,1692 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ + +package gov.nasa.worldwind.util;/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ + +import android.content.Context; +import android.content.pm.ConfigurationInfo; +import android.graphics.SurfaceTexture; +import android.opengl.GLDebugHelper; +import android.opengl.GLSurfaceView; +import android.opengl.GLUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.TextureView; + +import javax.microedition.khronos.egl.*; +import javax.microedition.khronos.opengles.GL; +import javax.microedition.khronos.opengles.GL10; +import java.io.Writer; +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +/** + * Created by kedzie on 4/12/14. + */ +public class GLTextureView extends TextureView implements TextureView.SurfaceTextureListener { + private final static String TAG = "GLTextureView"; + private final static boolean LOG_ATTACH_DETACH = false; + private final static boolean LOG_THREADS = false; + private final static boolean LOG_PAUSE_RESUME = false; + private final static boolean LOG_SURFACE = false; + private final static boolean LOG_RENDERER = false; + private final static boolean LOG_RENDERER_DRAW_FRAME = false; + private final static boolean LOG_EGL = false; + /** + * The renderer only renders + * when the surface is created, or when {@link #requestRender} is called. + * + * @see #getRenderMode() + * @see #setRenderMode(int) + * @see #requestRender() + */ + public final static int RENDERMODE_WHEN_DIRTY = 0; + /** + * The renderer is called + * continuously to re-render the scene. + * + * @see #getRenderMode() + * @see #setRenderMode(int) + */ + public final static int RENDERMODE_CONTINUOUSLY = 1; + + /** + * Check glError() after every GL call and throw an exception if glError indicates + * that an error has occurred. This can be used to help track down which OpenGL ES call + * is causing an error. + * + * @see #getDebugFlags + * @see #setDebugFlags + */ + public final static int DEBUG_CHECK_GL_ERROR = 1; + + /** + * Log GL calls to the system log at "verbose" level with tag "GLTextureView". + * + * @see #getDebugFlags + * @see #setDebugFlags + */ + public final static int DEBUG_LOG_GL_CALLS = 2; + + /** + * Standard View constructor. In order to render something, you + * must call {@link #setRenderer} to register a renderer. + */ + public GLTextureView(Context context) { + super(context); + init(); + } + + /** + * Standard View econstructor. In order to render something, you + * must call {@link #setRenderer} to register a renderer. + */ + public GLTextureView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + @Override + protected void finalize() throws Throwable { + try { + if (mGLThread != null) { + // GLThread may still be running if this view was never + // attached to a window. + mGLThread.requestExitAndWait(); + } + } finally { + super.finalize(); + } + } + + private void init() { + setSurfaceTextureListener(this); + } + + /** + * Set the glWrapper. If the glWrapper is not null, its + * {@link GLWrapper#wrap(javax.microedition.khronos.opengles.GL)} method is called + * whenever a surface is created. A GLWrapper can be used to wrap + * the GL object that's passed to the renderer. Wrapping a GL + * object enables examining and modifying the behavior of the + * GL calls made by the renderer. + *

    + * Wrapping is typically used for debugging purposes. + *

    + * The default value is null. + * @param glWrapper the new GLWrapper + */ + public void setGLWrapper(GLWrapper glWrapper) { + mGLWrapper = glWrapper; + } + + /** + * Set the debug flags to a new value. The value is + * constructed by OR-together zero or more + * of the DEBUG_CHECK_* constants. The debug flags take effect + * whenever a surface is created. The default value is zero. + * @param debugFlags the new debug flags + * @see #DEBUG_CHECK_GL_ERROR + * @see #DEBUG_LOG_GL_CALLS + */ + public void setDebugFlags(int debugFlags) { + mDebugFlags = debugFlags; + } + + /** + * Get the current value of the debug flags. + * @return the current value of the debug flags. + */ + public int getDebugFlags() { + return mDebugFlags; + } + + /** + * Control whether the EGL context is preserved when the GLTextureView is paused and + * resumed. + *

    + * If set to true, then the EGL context may be preserved when the GLTextureView is paused. + * Whether the EGL context is actually preserved or not depends upon whether the + * Android device that the program is running on can support an arbitrary number of EGL + * contexts or not. Devices that can only support a limited number of EGL contexts must + * release the EGL context in order to allow multiple applications to share the GPU. + *

    + * If set to false, the EGL context will be released when the GLTextureView is paused, + * and recreated when the GLTextureView is resumed. + *

    + * + * The default is false. + * + * @param preserveOnPause preserve the EGL context when paused + */ + public void setPreserveEGLContextOnPause(boolean preserveOnPause) { + mPreserveEGLContextOnPause = preserveOnPause; + } + + /** + * @return true if the EGL context will be preserved when paused + */ + public boolean getPreserveEGLContextOnPause() { + return mPreserveEGLContextOnPause; + } + + /** + * Set the renderer associated with this view. Also starts the thread that + * will call the renderer, which in turn causes the rendering to start. + *

    This method should be called once and only once in the life-cycle of + * a GLTextureView. + *

    The following GLTextureView methods can only be called before + * setRenderer is called: + *

      + *
    • {@link #setEGLConfigChooser(boolean)} + *
    • {@link #setEGLConfigChooser(EGLConfigChooser)} + *
    • {@link #setEGLConfigChooser(int, int, int, int, int, int)} + *
    + *

    + * The following GLTextureView methods can only be called after + * setRenderer is called: + *

      + *
    • {@link #getRenderMode()} + *
    • {@link #onPause()} + *
    • {@link #onResume()} + *
    • {@link #queueEvent(Runnable)} + *
    • {@link #requestRender()} + *
    • {@link #setRenderMode(int)} + *
    + * + * @param renderer the renderer to use to perform OpenGL drawing. + */ + public void setRenderer(GLSurfaceView.Renderer renderer) { + checkRenderThreadState(); + if (mEGLConfigChooser == null) { + mEGLConfigChooser = new SimpleEGLConfigChooser(true); + } + if (mEGLContextFactory == null) { + mEGLContextFactory = new DefaultContextFactory(); + } + if (mEGLWindowSurfaceFactory == null) { + mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory(); + } + mRenderer = renderer; + mGLThread = new GLThread(mThisWeakRef); + mGLThread.start(); + } + + /** + * Install a custom EGLContextFactory. + *

    If this method is + * called, it must be called before {@link #setRenderer(android.opengl.GLSurfaceView.Renderer)} + * is called. + *

    + * If this method is not called, then by default + * a context will be created with no shared context and + * with a null attribute list. + */ + public void setEGLContextFactory(EGLContextFactory factory) { + checkRenderThreadState(); + mEGLContextFactory = factory; + } + + /** + * Install a custom EGLWindowSurfaceFactory. + *

    If this method is + * called, it must be called before {@link #setRenderer(android.opengl.GLSurfaceView.Renderer)} + * is called. + *

    + * If this method is not called, then by default + * a window surface will be created with a null attribute list. + */ + public void setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory factory) { + checkRenderThreadState(); + mEGLWindowSurfaceFactory = factory; + } + + /** + * Install a custom EGLConfigChooser. + *

    If this method is + * called, it must be called before {@link #setRenderer(android.opengl.GLSurfaceView.Renderer)} + * is called. + *

    + * If no setEGLConfigChooser method is called, then by default the + * view will choose an EGLConfig that is compatible with the current + * android.view.Surface, with a depth buffer depth of + * at least 16 bits. + * @param configChooser + */ + public void setEGLConfigChooser(EGLConfigChooser configChooser) { + checkRenderThreadState(); + mEGLConfigChooser = configChooser; + } + + /** + * Install a config chooser which will choose a config + * as close to 16-bit RGB as possible, with or without an optional depth + * buffer as close to 16-bits as possible. + *

    If this method is + * called, it must be called before {@link #setRenderer(android.opengl.GLSurfaceView.Renderer)} + * is called. + *

    + * If no setEGLConfigChooser method is called, then by default the + * view will choose an RGB_888 surface with a depth buffer depth of + * at least 16 bits. + * + * @param needDepth + */ + public void setEGLConfigChooser(boolean needDepth) { + setEGLConfigChooser(new SimpleEGLConfigChooser(needDepth)); + } + + /** + * Install a config chooser which will choose a config + * with at least the specified depthSize and stencilSize, + * and exactly the specified redSize, greenSize, blueSize and alphaSize. + *

    If this method is + * called, it must be called before {@link #setRenderer(android.opengl.GLSurfaceView.Renderer)} + * is called. + *

    + * If no setEGLConfigChooser method is called, then by default the + * view will choose an RGB_888 surface with a depth buffer depth of + * at least 16 bits. + * + */ + public void setEGLConfigChooser(int redSize, int greenSize, int blueSize, + int alphaSize, int depthSize, int stencilSize) { + setEGLConfigChooser(new ComponentSizeChooser(redSize, greenSize, + blueSize, alphaSize, depthSize, stencilSize)); + } + + /** + * Inform the default EGLContextFactory and default EGLConfigChooser + * which EGLContext client version to pick. + *

    Use this method to create an OpenGL ES 2.0-compatible context. + * Example: + *

    +	 *     public MyView(Context context) {
    +	 *         super(context);
    +	 *         setEGLContextClientVersion(2); // Pick an OpenGL ES 2.0 context.
    +	 *         setRenderer(new MyRenderer());
    +	 *     }
    +	 * 
    + *

    Note: Activities which require OpenGL ES 2.0 should indicate this by + * setting @lt;uses-feature android:glEsVersion="0x00020000" /> in the activity's + * AndroidManifest.xml file. + *

    If this method is called, it must be called before {@link #setRenderer(android.opengl.GLSurfaceView.Renderer)} + * is called. + *

    This method only affects the behavior of the default EGLContexFactory and the + * default EGLConfigChooser. If + * {@link #setEGLContextFactory(EGLContextFactory)} has been called, then the supplied + * EGLContextFactory is responsible for creating an OpenGL ES 2.0-compatible context. + * If + * {@link #setEGLConfigChooser(EGLConfigChooser)} has been called, then the supplied + * EGLConfigChooser is responsible for choosing an OpenGL ES 2.0-compatible config. + * @param version The EGLContext client version to choose. Use 2 for OpenGL ES 2.0 + */ + public void setEGLContextClientVersion(int version) { + checkRenderThreadState(); + mEGLContextClientVersion = version; + } + + /** + * Set the rendering mode. When renderMode is + * RENDERMODE_CONTINUOUSLY, the renderer is called + * repeatedly to re-render the scene. When renderMode + * is RENDERMODE_WHEN_DIRTY, the renderer only rendered when the surface + * is created, or when {@link #requestRender} is called. Defaults to RENDERMODE_CONTINUOUSLY. + *

    + * Using RENDERMODE_WHEN_DIRTY can improve battery life and overall system performance + * by allowing the GPU and CPU to idle when the view does not need to be updated. + *

    + * This method can only be called after {@link #setRenderer(android.opengl.GLSurfaceView.Renderer)} + * + * @param renderMode one of the RENDERMODE_X constants + * @see #RENDERMODE_CONTINUOUSLY + * @see #RENDERMODE_WHEN_DIRTY + */ + public void setRenderMode(int renderMode) { + mGLThread.setRenderMode(renderMode); + } + + /** + * Get the current rendering mode. May be called + * from any thread. Must not be called before a renderer has been set. + * @return the current rendering mode. + * @see #RENDERMODE_CONTINUOUSLY + * @see #RENDERMODE_WHEN_DIRTY + */ + public int getRenderMode() { + return mGLThread.getRenderMode(); + } + + /** + * Request that the renderer render a frame. + * This method is typically used when the render mode has been set to + * {@link #RENDERMODE_WHEN_DIRTY}, so that frames are only rendered on demand. + * May be called + * from any thread. Must not be called before a renderer has been set. + */ + public void requestRender() { + mGLThread.requestRender(); + } + + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + mGLThread.surfaceCreated(); + mGLThread.onWindowResize(width, height); + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + mGLThread.onWindowResize(width, height); + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + // Surface will be destroyed when we return + mGLThread.surfaceDestroyed(); + return true; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + + } + + /** + * Inform the view that the activity is paused. The owner of this view must + * call this method when the activity is paused. Calling this method will + * pause the rendering thread. + * Must not be called before a renderer has been set. + */ + public void onPause() { + mGLThread.onPause(); + } + + /** + * Inform the view that the activity is resumed. The owner of this view must + * call this method when the activity is resumed. Calling this method will + * recreate the OpenGL display and resume the rendering + * thread. + * Must not be called before a renderer has been set. + */ + public void onResume() { + mGLThread.onResume(); + } + + /** + * Queue a runnable to be run on the GL rendering thread. This can be used + * to communicate with the Renderer on the rendering thread. + * Must not be called before a renderer has been set. + * @param r the runnable to be run on the GL rendering thread. + */ + public void queueEvent(Runnable r) { + mGLThread.queueEvent(r); + } + + /** + * This method is used as part of the View class and is not normally + * called or subclassed by clients of GLTextureView. + */ + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (LOG_ATTACH_DETACH) { + Log.d(TAG, "onAttachedToWindow reattach =" + mDetached); + } + if (mDetached && (mRenderer != null)) { + int renderMode = RENDERMODE_CONTINUOUSLY; + if (mGLThread != null) { + renderMode = mGLThread.getRenderMode(); + } + mGLThread = new GLThread(mThisWeakRef); + if (renderMode != RENDERMODE_CONTINUOUSLY) { + mGLThread.setRenderMode(renderMode); + } + mGLThread.start(); + } + mDetached = false; + } + + /** + * This method is used as part of the View class and is not normally + * called or subclassed by clients of GLTextureView. + * Must not be called before a renderer has been set. + */ + @Override + protected void onDetachedFromWindow() { + if (LOG_ATTACH_DETACH) { + Log.d(TAG, "onDetachedFromWindow"); + } + if (mGLThread != null) { + mGLThread.requestExitAndWait(); + } + mDetached = true; + super.onDetachedFromWindow(); + } + + // ---------------------------------------------------------------------- + + /** + * An interface used to wrap a GL interface. + *

    Typically + * used for implementing debugging and tracing on top of the default + * GL interface. You would typically use this by creating your own class + * that implemented all the GL methods by delegating to another GL instance. + * Then you could add your own behavior before or after calling the + * delegate. All the GLWrapper would do was instantiate and return the + * wrapper GL instance: + *

    +	 * class MyGLWrapper implements GLWrapper {
    +	 *     GL wrap(GL gl) {
    +	 *         return new MyGLImplementation(gl);
    +	 *     }
    +	 *     static class MyGLImplementation implements GL,GL10,GL11,... {
    +	 *         ...
    +	 *     }
    +	 * }
    +	 * 
    + * @see #setGLWrapper(GLWrapper) + */ + public interface GLWrapper { + /** + * Wraps a gl interface in another gl interface. + * @param gl a GL interface that is to be wrapped. + * @return either the input argument or another GL object that wraps the input argument. + */ + GL wrap(GL gl); + } + + /** + * An interface for customizing the eglCreateContext and eglDestroyContext calls. + *

    + * This interface must be implemented by clients wishing to call + * {@link GLTextureView#setEGLContextFactory(EGLContextFactory)} + */ + public interface EGLContextFactory { + EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig); + void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context); + } + + private class DefaultContextFactory implements EGLContextFactory { + private int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + + public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config) { + int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion, + EGL10.EGL_NONE }; + + return egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, + mEGLContextClientVersion != 0 ? attrib_list : null); + } + + public void destroyContext(EGL10 egl, EGLDisplay display, + EGLContext context) { + if (!egl.eglDestroyContext(display, context)) { + Log.e("DefaultContextFactory", "display:" + display + " context: " + context); + if (LOG_THREADS) { + Log.i("DefaultContextFactory", "tid=" + Thread.currentThread().getId()); + } + EglHelper.throwEglException("eglDestroyContex", egl.eglGetError()); + } + } + } + + /** + * An interface for customizing the eglCreateWindowSurface and eglDestroySurface calls. + *

    + * This interface must be implemented by clients wishing to call + * {@link GLTextureView#setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory)} + */ + public interface EGLWindowSurfaceFactory { + /** + * @return null if the surface cannot be constructed. + */ + EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config, + Object nativeWindow); + void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface); + } + + private static class DefaultWindowSurfaceFactory implements EGLWindowSurfaceFactory { + + public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, + EGLConfig config, Object nativeWindow) { + EGLSurface result = null; + try { + result = egl.eglCreateWindowSurface(display, config, nativeWindow, null); + } catch (IllegalArgumentException e) { + // This exception indicates that the surface flinger surface + // is not valid. This can happen if the surface flinger surface has + // been torn down, but the application has not yet been + // notified via SurfaceHolder.Callback.surfaceDestroyed. + // In theory the application should be notified first, + // but in practice sometimes it is not. See b/4588890 + Log.e(TAG, "eglCreateWindowSurface", e); + } + return result; + } + + public void destroySurface(EGL10 egl, EGLDisplay display, + EGLSurface surface) { + egl.eglDestroySurface(display, surface); + } + } + + /** + * An interface for choosing an EGLConfig configuration from a list of + * potential configurations. + *

    + * This interface must be implemented by clients wishing to call + * {@link GLTextureView#setEGLConfigChooser(EGLConfigChooser)} + */ + public interface EGLConfigChooser { + /** + * Choose a configuration from the list. Implementors typically + * implement this method by calling + * {@link javax.microedition.khronos.egl.EGL10#eglChooseConfig} and iterating through the results. Please consult the + * EGL specification available from The Khronos Group to learn how to call eglChooseConfig. + * @param egl the EGL10 for the current display. + * @param display the current display. + * @return the chosen configuration. + */ + EGLConfig chooseConfig(EGL10 egl, EGLDisplay display); + } + + private abstract class BaseConfigChooser + implements EGLConfigChooser { + public BaseConfigChooser(int[] configSpec) { + mConfigSpec = filterConfigSpec(configSpec); + } + + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { + int[] num_config = new int[1]; + if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, + num_config)) { + throw new IllegalArgumentException("eglChooseConfig failed"); + } + + int numConfigs = num_config[0]; + + if (numConfigs <= 0) { + throw new IllegalArgumentException( + "No configs match configSpec"); + } + + EGLConfig[] configs = new EGLConfig[numConfigs]; + if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, + num_config)) { + throw new IllegalArgumentException("eglChooseConfig#2 failed"); + } + EGLConfig config = chooseConfig(egl, display, configs); + if (config == null) { + throw new IllegalArgumentException("No config chosen"); + } + return config; + } + + abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, + EGLConfig[] configs); + + protected int[] mConfigSpec; + + private int[] filterConfigSpec(int[] configSpec) { + if (mEGLContextClientVersion != 2) { + return configSpec; + } + /* We know none of the subclasses define EGL_RENDERABLE_TYPE. + * And we know the configSpec is well formed. + */ + int len = configSpec.length; + int[] newConfigSpec = new int[len + 2]; + System.arraycopy(configSpec, 0, newConfigSpec, 0, len-1); + newConfigSpec[len-1] = EGL10.EGL_RENDERABLE_TYPE; + newConfigSpec[len] = 4; /* EGL_OPENGL_ES2_BIT */ + newConfigSpec[len+1] = EGL10.EGL_NONE; + return newConfigSpec; + } + } + + /** + * Choose a configuration with exactly the specified r,g,b,a sizes, + * and at least the specified depth and stencil sizes. + */ + private class ComponentSizeChooser extends BaseConfigChooser { + public ComponentSizeChooser(int redSize, int greenSize, int blueSize, + int alphaSize, int depthSize, int stencilSize) { + super(new int[] { + EGL10.EGL_RED_SIZE, redSize, + EGL10.EGL_GREEN_SIZE, greenSize, + EGL10.EGL_BLUE_SIZE, blueSize, + EGL10.EGL_ALPHA_SIZE, alphaSize, + EGL10.EGL_DEPTH_SIZE, depthSize, + EGL10.EGL_STENCIL_SIZE, stencilSize, + EGL10.EGL_NONE}); + mValue = new int[1]; + mRedSize = redSize; + mGreenSize = greenSize; + mBlueSize = blueSize; + mAlphaSize = alphaSize; + mDepthSize = depthSize; + mStencilSize = stencilSize; + } + + @Override + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, + EGLConfig[] configs) { + for (EGLConfig config : configs) { + int d = findConfigAttrib(egl, display, config, + EGL10.EGL_DEPTH_SIZE, 0); + int s = findConfigAttrib(egl, display, config, + EGL10.EGL_STENCIL_SIZE, 0); + if ((d >= mDepthSize) && (s >= mStencilSize)) { + int r = findConfigAttrib(egl, display, config, + EGL10.EGL_RED_SIZE, 0); + int g = findConfigAttrib(egl, display, config, + EGL10.EGL_GREEN_SIZE, 0); + int b = findConfigAttrib(egl, display, config, + EGL10.EGL_BLUE_SIZE, 0); + int a = findConfigAttrib(egl, display, config, + EGL10.EGL_ALPHA_SIZE, 0); + if ((r == mRedSize) && (g == mGreenSize) + && (b == mBlueSize) && (a == mAlphaSize)) { + return config; + } + } + } + return null; + } + + private int findConfigAttrib(EGL10 egl, EGLDisplay display, + EGLConfig config, int attribute, int defaultValue) { + + if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { + return mValue[0]; + } + return defaultValue; + } + + private int[] mValue; + // Subclasses can adjust these values: + protected int mRedSize; + protected int mGreenSize; + protected int mBlueSize; + protected int mAlphaSize; + protected int mDepthSize; + protected int mStencilSize; + } + + /** + * This class will choose a RGB_888 surface with + * or without a depth buffer. + * + */ + private class SimpleEGLConfigChooser extends ComponentSizeChooser { + public SimpleEGLConfigChooser(boolean withDepthBuffer) { + super(8, 8, 8, 0, withDepthBuffer ? 16 : 0, 0); + } + } + + /** + * An EGL helper class. + */ + + private static class EglHelper { + public EglHelper(WeakReference glSurfaceViewWeakRef) { + mGLSurfaceViewWeakRef = glSurfaceViewWeakRef; + } + + /** + * Initialize EGL for a given configuration spec. + */ + public void start() { + if (LOG_EGL) { + Log.w("EglHelper", "start() tid=" + Thread.currentThread().getId()); + } + /* + * Get an EGL instance + */ + mEgl = (EGL10) EGLContext.getEGL(); + + /* + * Get to the default display. + */ + mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + + if (mEglDisplay == EGL10.EGL_NO_DISPLAY) { + throw new RuntimeException("eglGetDisplay failed"); + } + + /* + * We can now initialize EGL for that display + */ + int[] version = new int[2]; + if(!mEgl.eglInitialize(mEglDisplay, version)) { + throw new RuntimeException("eglInitialize failed"); + } + GLTextureView view = mGLSurfaceViewWeakRef.get(); + if (view == null) { + mEglConfig = null; + mEglContext = null; + } else { + mEglConfig = view.mEGLConfigChooser.chooseConfig(mEgl, mEglDisplay); + + /* + * Create an EGL context. We want to do this as rarely as we can, because an + * EGL context is a somewhat heavy object. + */ + mEglContext = view.mEGLContextFactory.createContext(mEgl, mEglDisplay, mEglConfig); + } + if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) { + mEglContext = null; + throwEglException("createContext"); + } + if (LOG_EGL) { + Log.w("EglHelper", "createContext " + mEglContext + " tid=" + Thread.currentThread().getId()); + } + + mEglSurface = null; + } + + /** + * Create an egl surface for the current SurfaceHolder surface. If a surface + * already exists, destroy it before creating the new surface. + * + * @return true if the surface was created successfully. + */ + public boolean createSurface() { + if (LOG_EGL) { + Log.w("EglHelper", "createSurface() tid=" + Thread.currentThread().getId()); + } + /* + * Check preconditions. + */ + if (mEgl == null) { + throw new RuntimeException("egl not initialized"); + } + if (mEglDisplay == null) { + throw new RuntimeException("eglDisplay not initialized"); + } + if (mEglConfig == null) { + throw new RuntimeException("mEglConfig not initialized"); + } + + /* + * The window size has changed, so we need to create a new + * surface. + */ + destroySurfaceImp(); + + /* + * Create an EGL surface we can render into. + */ + GLTextureView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + mEglSurface = view.mEGLWindowSurfaceFactory.createWindowSurface(mEgl, + mEglDisplay, mEglConfig, view.getSurfaceTexture()); + } else { + mEglSurface = null; + } + + if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) { + int error = mEgl.eglGetError(); + if (error == EGL10.EGL_BAD_NATIVE_WINDOW) { + Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW."); + } + return false; + } + + /* + * Before we can issue GL commands, we need to make sure + * the context is current and bound to a surface. + */ + if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) { + /* + * Could not make the context current, probably because the underlying + * SurfaceView surface has been destroyed. + */ + logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError()); + return false; + } + + return true; + } + + /** + * Create a GL object for the current EGL context. + * @return + */ + GL createGL() { + + GL gl = mEglContext.getGL(); + GLTextureView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + if (view.mGLWrapper != null) { + gl = view.mGLWrapper.wrap(gl); + } + + if ((view.mDebugFlags & (DEBUG_CHECK_GL_ERROR | DEBUG_LOG_GL_CALLS)) != 0) { + int configFlags = 0; + Writer log = null; + if ((view.mDebugFlags & DEBUG_CHECK_GL_ERROR) != 0) { + configFlags |= GLDebugHelper.CONFIG_CHECK_GL_ERROR; + } + if ((view.mDebugFlags & DEBUG_LOG_GL_CALLS) != 0) { + log = new LogWriter(); + } + gl = GLDebugHelper.wrap(gl, configFlags, log); + } + } + return gl; + } + + /** + * Display the current render surface. + * @return the EGL error code from eglSwapBuffers. + */ + public int swap() { + if (! mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) { + return mEgl.eglGetError(); + } + return EGL10.EGL_SUCCESS; + } + + public void destroySurface() { + if (LOG_EGL) { + Log.w("EglHelper", "destroySurface() tid=" + Thread.currentThread().getId()); + } + destroySurfaceImp(); + } + + private void destroySurfaceImp() { + if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) { + mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_CONTEXT); + GLTextureView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mEGLWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface); + } + mEglSurface = null; + } + } + + public void finish() { + if (LOG_EGL) { + Log.w("EglHelper", "finish() tid=" + Thread.currentThread().getId()); + } + if (mEglContext != null) { + GLTextureView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mEGLContextFactory.destroyContext(mEgl, mEglDisplay, mEglContext); + } + mEglContext = null; + } + if (mEglDisplay != null) { + mEgl.eglTerminate(mEglDisplay); + mEglDisplay = null; + } + } + + private void throwEglException(String function) { + throwEglException(function, mEgl.eglGetError()); + } + + public static void throwEglException(String function, int error) { + String message = formatEglError(function, error); + if (LOG_THREADS) { + Log.e("EglHelper", "throwEglException tid=" + Thread.currentThread().getId() + " " + + message); + } + throw new RuntimeException(message); + } + + public static void logEglErrorAsWarning(String tag, String function, int error) { + Log.w(tag, formatEglError(function, error)); + } + + public static String formatEglError(String function, int error) { + return function + " failed: " + GLUtils.getEGLErrorString(error); + } + + private WeakReference mGLSurfaceViewWeakRef; + EGL10 mEgl; + EGLDisplay mEglDisplay; + EGLSurface mEglSurface; + EGLConfig mEglConfig; + EGLContext mEglContext; + + } + + /** + * A generic GL Thread. Takes care of initializing EGL and GL. Delegates + * to a Renderer instance to do the actual drawing. Can be configured to + * render continuously or on request. + * + * All potentially blocking synchronization is done through the + * sGLThreadManager object. This avoids multiple-lock ordering issues. + * + */ + static class GLThread extends Thread { + GLThread(WeakReference glSurfaceViewWeakRef) { + super(); + mWidth = 0; + mHeight = 0; + mRequestRender = true; + mRenderMode = RENDERMODE_CONTINUOUSLY; + mGLSurfaceViewWeakRef = glSurfaceViewWeakRef; + } + + @Override + public void run() { + setName("GLThread " + getId()); + if (LOG_THREADS) { + Log.i("GLThread", "starting tid=" + getId()); + } + + try { + guardedRun(); + } catch (InterruptedException e) { + // fall thru and exit normally + } finally { + sGLThreadManager.threadExiting(this); + } + } + + /* + * This private method should only be called inside a + * synchronized(sGLThreadManager) block. + */ + private void stopEglSurfaceLocked() { + if (mHaveEglSurface) { + mHaveEglSurface = false; + mEglHelper.destroySurface(); + } + } + + /* + * This private method should only be called inside a + * synchronized(sGLThreadManager) block. + */ + private void stopEglContextLocked() { + if (mHaveEglContext) { + mEglHelper.finish(); + mHaveEglContext = false; + sGLThreadManager.releaseEglContextLocked(this); + } + } + private void guardedRun() throws InterruptedException { + mEglHelper = new EglHelper(mGLSurfaceViewWeakRef); + mHaveEglContext = false; + mHaveEglSurface = false; + try { + GL10 gl = null; + boolean createEglContext = false; + boolean createEglSurface = false; + boolean createGlInterface = false; + boolean lostEglContext = false; + boolean sizeChanged = false; + boolean wantRenderNotification = false; + boolean doRenderNotification = false; + boolean askedToReleaseEglContext = false; + int w = 0; + int h = 0; + Runnable event = null; + + while (true) { + synchronized (sGLThreadManager) { + while (true) { + if (mShouldExit) { + return; + } + + if (! mEventQueue.isEmpty()) { + event = mEventQueue.remove(0); + break; + } + + // Update the pause state. + boolean pausing = false; + if (mPaused != mRequestPaused) { + pausing = mRequestPaused; + mPaused = mRequestPaused; + sGLThreadManager.notifyAll(); + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "mPaused is now " + mPaused + " tid=" + getId()); + } + } + + // Do we need to give up the EGL context? + if (mShouldReleaseEglContext) { + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL context because asked to tid=" + getId()); + } + stopEglSurfaceLocked(); + stopEglContextLocked(); + mShouldReleaseEglContext = false; + askedToReleaseEglContext = true; + } + + // Have we lost the EGL context? + if (lostEglContext) { + stopEglSurfaceLocked(); + stopEglContextLocked(); + lostEglContext = false; + } + + // When pausing, release the EGL surface: + if (pausing && mHaveEglSurface) { + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL surface because paused tid=" + getId()); + } + stopEglSurfaceLocked(); + } + + // When pausing, optionally release the EGL Context: + if (pausing && mHaveEglContext) { + GLTextureView view = mGLSurfaceViewWeakRef.get(); + boolean preserveEglContextOnPause = view == null ? + false : view.mPreserveEGLContextOnPause; + if (!preserveEglContextOnPause || sGLThreadManager.shouldReleaseEGLContextWhenPausing()) { + stopEglContextLocked(); + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL context because paused tid=" + getId()); + } + } + } + + // When pausing, optionally terminate EGL: + if (pausing) { + if (sGLThreadManager.shouldTerminateEGLWhenPausing()) { + mEglHelper.finish(); + if (LOG_SURFACE) { + Log.i("GLThread", "terminating EGL because paused tid=" + getId()); + } + } + } + + // Have we lost the SurfaceView surface? + if ((! mHasSurface) && (! mWaitingForSurface)) { + if (LOG_SURFACE) { + Log.i("GLThread", "noticed surfaceView surface lost tid=" + getId()); + } + if (mHaveEglSurface) { + stopEglSurfaceLocked(); + } + mWaitingForSurface = true; + mSurfaceIsBad = false; + sGLThreadManager.notifyAll(); + } + + // Have we acquired the surface view surface? + if (mHasSurface && mWaitingForSurface) { + if (LOG_SURFACE) { + Log.i("GLThread", "noticed surfaceView surface acquired tid=" + getId()); + } + mWaitingForSurface = false; + sGLThreadManager.notifyAll(); + } + + if (doRenderNotification) { + if (LOG_SURFACE) { + Log.i("GLThread", "sending render notification tid=" + getId()); + } + wantRenderNotification = false; + doRenderNotification = false; + mRenderComplete = true; + sGLThreadManager.notifyAll(); + } + + // Ready to draw? + if (readyToDraw()) { + + // If we don't have an EGL context, try to acquire one. + if (! mHaveEglContext) { + if (askedToReleaseEglContext) { + askedToReleaseEglContext = false; + } else if (sGLThreadManager.tryAcquireEglContextLocked(this)) { + try { + mEglHelper.start(); + } catch (RuntimeException t) { + sGLThreadManager.releaseEglContextLocked(this); + throw t; + } + mHaveEglContext = true; + createEglContext = true; + + sGLThreadManager.notifyAll(); + } + } + + if (mHaveEglContext && !mHaveEglSurface) { + mHaveEglSurface = true; + createEglSurface = true; + createGlInterface = true; + sizeChanged = true; + } + + if (mHaveEglSurface) { + if (mSizeChanged) { + sizeChanged = true; + w = mWidth; + h = mHeight; + wantRenderNotification = true; + if (LOG_SURFACE) { + Log.i("GLThread", + "noticing that we want render notification tid=" + + getId()); + } + + // Destroy and recreate the EGL surface. + createEglSurface = true; + + mSizeChanged = false; + } + mRequestRender = false; + sGLThreadManager.notifyAll(); + break; + } + } + + // By design, this is the only place in a GLThread thread where we wait(). + if (LOG_THREADS) { + Log.i("GLThread", "waiting tid=" + getId() + + " mHaveEglContext: " + mHaveEglContext + + " mHaveEglSurface: " + mHaveEglSurface + + " mFinishedCreatingEglSurface: " + mFinishedCreatingEglSurface + + " mPaused: " + mPaused + + " mHasSurface: " + mHasSurface + + " mSurfaceIsBad: " + mSurfaceIsBad + + " mWaitingForSurface: " + mWaitingForSurface + + " mWidth: " + mWidth + + " mHeight: " + mHeight + + " mRequestRender: " + mRequestRender + + " mRenderMode: " + mRenderMode); + } + sGLThreadManager.wait(); + } + } // end of synchronized(sGLThreadManager) + + if (event != null) { + event.run(); + event = null; + continue; + } + + if (createEglSurface) { + if (LOG_SURFACE) { + Log.w("GLThread", "egl createSurface"); + } + if (mEglHelper.createSurface()) { + synchronized(sGLThreadManager) { + mFinishedCreatingEglSurface = true; + sGLThreadManager.notifyAll(); + } + } else { + synchronized(sGLThreadManager) { + mFinishedCreatingEglSurface = true; + mSurfaceIsBad = true; + sGLThreadManager.notifyAll(); + } + continue; + } + createEglSurface = false; + } + + if (createGlInterface) { + gl = (GL10) mEglHelper.createGL(); + + sGLThreadManager.checkGLDriver(gl); + createGlInterface = false; + } + + if (createEglContext) { + if (LOG_RENDERER) { + Log.w("GLThread", "onSurfaceCreated"); + } + GLTextureView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig); + } + createEglContext = false; + } + + if (sizeChanged) { + if (LOG_RENDERER) { + Log.w("GLThread", "onSurfaceChanged(" + w + ", " + h + ")"); + } + GLTextureView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mRenderer.onSurfaceChanged(gl, w, h); + } + sizeChanged = false; + } + + if (LOG_RENDERER_DRAW_FRAME) { + Log.w("GLThread", "onDrawFrame tid=" + getId()); + } + { + GLTextureView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mRenderer.onDrawFrame(gl); + } + } + int swapError = mEglHelper.swap(); + switch (swapError) { + case EGL10.EGL_SUCCESS: + break; + case EGL11.EGL_CONTEXT_LOST: + if (LOG_SURFACE) { + Log.i("GLThread", "egl context lost tid=" + getId()); + } + lostEglContext = true; + break; + default: + // Other errors typically mean that the current surface is bad, + // probably because the SurfaceView surface has been destroyed, + // but we haven't been notified yet. + // Log the error to help developers understand why rendering stopped. + EglHelper.logEglErrorAsWarning("GLThread", "eglSwapBuffers", swapError); + + synchronized(sGLThreadManager) { + mSurfaceIsBad = true; + sGLThreadManager.notifyAll(); + } + break; + } + + if (wantRenderNotification) { + doRenderNotification = true; + } + } + + } finally { + /* + * clean-up everything... + */ + synchronized (sGLThreadManager) { + stopEglSurfaceLocked(); + stopEglContextLocked(); + } + } + } + + public boolean ableToDraw() { + return mHaveEglContext && mHaveEglSurface && readyToDraw(); + } + + private boolean readyToDraw() { + return (!mPaused) && mHasSurface && (!mSurfaceIsBad) + && (mWidth > 0) && (mHeight > 0) + && (mRequestRender || (mRenderMode == RENDERMODE_CONTINUOUSLY)); + } + + public void setRenderMode(int renderMode) { + if ( !((RENDERMODE_WHEN_DIRTY <= renderMode) && (renderMode <= RENDERMODE_CONTINUOUSLY)) ) { + throw new IllegalArgumentException("renderMode"); + } + synchronized(sGLThreadManager) { + mRenderMode = renderMode; + sGLThreadManager.notifyAll(); + } + } + + public int getRenderMode() { + synchronized(sGLThreadManager) { + return mRenderMode; + } + } + + public void requestRender() { + synchronized(sGLThreadManager) { + mRequestRender = true; + sGLThreadManager.notifyAll(); + } + } + + public void surfaceCreated() { + synchronized(sGLThreadManager) { + if (LOG_THREADS) { + Log.i("GLThread", "surfaceCreated tid=" + getId()); + } + mHasSurface = true; + mFinishedCreatingEglSurface = false; + sGLThreadManager.notifyAll(); + while (mWaitingForSurface + && !mFinishedCreatingEglSurface + && !mExited) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void surfaceDestroyed() { + synchronized(sGLThreadManager) { + if (LOG_THREADS) { + Log.i("GLThread", "surfaceDestroyed tid=" + getId()); + } + mHasSurface = false; + sGLThreadManager.notifyAll(); + while((!mWaitingForSurface) && (!mExited)) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onPause() { + synchronized (sGLThreadManager) { + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "onPause tid=" + getId()); + } + mRequestPaused = true; + sGLThreadManager.notifyAll(); + while ((! mExited) && (! mPaused)) { + if (LOG_PAUSE_RESUME) { + Log.i("Main thread", "onPause waiting for mPaused."); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onResume() { + synchronized (sGLThreadManager) { + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "onResume tid=" + getId()); + } + mRequestPaused = false; + mRequestRender = true; + mRenderComplete = false; + sGLThreadManager.notifyAll(); + while ((! mExited) && mPaused && (!mRenderComplete)) { + if (LOG_PAUSE_RESUME) { + Log.i("Main thread", "onResume waiting for !mPaused."); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onWindowResize(int w, int h) { + synchronized (sGLThreadManager) { + mWidth = w; + mHeight = h; + mSizeChanged = true; + mRequestRender = true; + mRenderComplete = false; + sGLThreadManager.notifyAll(); + + // Wait for thread to react to resize and render a frame + while (! mExited && !mPaused && !mRenderComplete + && ableToDraw()) { + if (LOG_SURFACE) { + Log.i("Main thread", "onWindowResize waiting for render complete from tid=" + getId()); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void requestExitAndWait() { + // don't call this from GLThread thread or it is a guaranteed + // deadlock! + synchronized(sGLThreadManager) { + mShouldExit = true; + sGLThreadManager.notifyAll(); + while (! mExited) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void requestReleaseEglContextLocked() { + mShouldReleaseEglContext = true; + sGLThreadManager.notifyAll(); + } + + /** + * Queue an "event" to be run on the GL rendering thread. + * @param r the runnable to be run on the GL rendering thread. + */ + public void queueEvent(Runnable r) { + if (r == null) { + throw new IllegalArgumentException("r must not be null"); + } + synchronized(sGLThreadManager) { + mEventQueue.add(r); + sGLThreadManager.notifyAll(); + } + } + + // Once the thread is started, all accesses to the following member + // variables are protected by the sGLThreadManager monitor + private boolean mShouldExit; + private boolean mExited; + private boolean mRequestPaused; + private boolean mPaused; + private boolean mHasSurface; + private boolean mSurfaceIsBad; + private boolean mWaitingForSurface; + private boolean mHaveEglContext; + private boolean mHaveEglSurface; + private boolean mFinishedCreatingEglSurface; + private boolean mShouldReleaseEglContext; + private int mWidth; + private int mHeight; + private int mRenderMode; + private boolean mRequestRender; + private boolean mRenderComplete; + private ArrayList mEventQueue = new ArrayList(); + private boolean mSizeChanged = true; + + // End of member variables protected by the sGLThreadManager monitor. + + private EglHelper mEglHelper; + + /** + * Set once at thread construction time, nulled out when the parent view is garbage + * called. This weak reference allows the GLTextureView to be garbage collected while + * the GLThread is still alive. + */ + private WeakReference mGLSurfaceViewWeakRef; + + } + + static class LogWriter extends Writer { + + @Override public void close() { + flushBuilder(); + } + + @Override public void flush() { + flushBuilder(); + } + + @Override public void write(char[] buf, int offset, int count) { + for(int i = 0; i < count; i++) { + char c = buf[offset + i]; + if ( c == '\n') { + flushBuilder(); + } + else { + mBuilder.append(c); + } + } + } + + private void flushBuilder() { + if (mBuilder.length() > 0) { + Log.v("GLTextureView", mBuilder.toString()); + mBuilder.delete(0, mBuilder.length()); + } + } + + private StringBuilder mBuilder = new StringBuilder(); + } + + + private void checkRenderThreadState() { + if (mGLThread != null) { + throw new IllegalStateException( + "setRenderer has already been called for this instance."); + } + } + + private static class GLThreadManager { + private static String TAG = "GLThreadManager"; + + public synchronized void threadExiting(GLThread thread) { + if (LOG_THREADS) { + Log.i("GLThread", "exiting tid=" + thread.getId()); + } + thread.mExited = true; + if (mEglOwner == thread) { + mEglOwner = null; + } + notifyAll(); + } + + /* + * Tries once to acquire the right to use an EGL + * context. Does not block. Requires that we are already + * in the sGLThreadManager monitor when this is called. + * + * @return true if the right to use an EGL context was acquired. + */ + public boolean tryAcquireEglContextLocked(GLThread thread) { + if (mEglOwner == thread || mEglOwner == null) { + mEglOwner = thread; + notifyAll(); + return true; + } + checkGLESVersion(); + if (mMultipleGLESContextsAllowed) { + return true; + } + // Notify the owning thread that it should release the context. + // TODO: implement a fairness policy. Currently + // if the owning thread is drawing continuously it will just + // reacquire the EGL context. + if (mEglOwner != null) { + mEglOwner.requestReleaseEglContextLocked(); + } + return false; + } + + /* + * Releases the EGL context. Requires that we are already in the + * sGLThreadManager monitor when this is called. + */ + public void releaseEglContextLocked(GLThread thread) { + if (mEglOwner == thread) { + mEglOwner = null; + } + notifyAll(); + } + + public synchronized boolean shouldReleaseEGLContextWhenPausing() { + // Release the EGL context when pausing even if + // the hardware supports multiple EGL contexts. + // Otherwise the device could run out of EGL contexts. + return mLimitedGLESContexts; + } + + public synchronized boolean shouldTerminateEGLWhenPausing() { + checkGLESVersion(); + return !mMultipleGLESContextsAllowed; + } + + public synchronized void checkGLDriver(GL10 gl) { + if (! mGLESDriverCheckComplete) { + checkGLESVersion(); + String renderer = gl.glGetString(GL10.GL_RENDERER); + if (mGLESVersion < kGLES_20) { + mMultipleGLESContextsAllowed = + ! renderer.startsWith(kMSM7K_RENDERER_PREFIX); + notifyAll(); + } + mLimitedGLESContexts = !mMultipleGLESContextsAllowed; + if (LOG_SURFACE) { + Log.w(TAG, "checkGLDriver renderer = \"" + renderer + "\" multipleContextsAllowed = " + + mMultipleGLESContextsAllowed + + " mLimitedGLESContexts = " + mLimitedGLESContexts); + } + mGLESDriverCheckComplete = true; + } + } + + private void checkGLESVersion() { + if (! mGLESVersionCheckComplete) { + mGLESVersion = SystemPropertiesProxy.getInt( + "ro.opengles.version", + ConfigurationInfo.GL_ES_VERSION_UNDEFINED); + if (mGLESVersion >= kGLES_20) { + mMultipleGLESContextsAllowed = true; + } + if (LOG_SURFACE) { + Log.w(TAG, "checkGLESVersion mGLESVersion =" + + " " + mGLESVersion + " mMultipleGLESContextsAllowed = " + mMultipleGLESContextsAllowed); + } + mGLESVersionCheckComplete = true; + } + } + + /** + * This check was required for some pre-Android-3.0 hardware. Android 3.0 provides + * support for hardware-accelerated views, therefore multiple EGL contexts are + * supported on all Android 3.0+ EGL drivers. + */ + private boolean mGLESVersionCheckComplete; + private int mGLESVersion; + private boolean mGLESDriverCheckComplete; + private boolean mMultipleGLESContextsAllowed; + private boolean mLimitedGLESContexts; + private static final int kGLES_20 = 0x20000; + private static final String kMSM7K_RENDERER_PREFIX = + "Q3Dimension MSM7500 "; + private GLThread mEglOwner; + } + + private static final GLThreadManager sGLThreadManager = new GLThreadManager(); + + private final WeakReference mThisWeakRef = + new WeakReference(this); + private GLThread mGLThread; + private GLSurfaceView.Renderer mRenderer; + private boolean mDetached; + private EGLConfigChooser mEGLConfigChooser; + private EGLContextFactory mEGLContextFactory; + private EGLWindowSurfaceFactory mEGLWindowSurfaceFactory; + private GLWrapper mGLWrapper; + private int mDebugFlags; + private int mEGLContextClientVersion; + private boolean mPreserveEGLContextOnPause; +} + diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/util/GeometryBuilder.java b/WorldWindAndroid/src/gov/nasa/worldwind/util/GeometryBuilder.java new file mode 100644 index 0000000..2d1a79a --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/util/GeometryBuilder.java @@ -0,0 +1,7517 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.util; + +import gov.nasa.worldwind.geom.Angle; +import gov.nasa.worldwind.geom.Vec4; + +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.HashMap; +import java.util.Map; + +import static android.opengl.GLES20.*; + +/** + * @author dcollins + * @version $Id: GeometryBuilder.java 1171 2013-02-11 21:45:02Z dcollins $ + */ +public class GeometryBuilder +{ + public static final int OUTSIDE = 0; + public static final int INSIDE = 1; + + public static final int COUNTER_CLOCKWISE = 0; + public static final int CLOCKWISE = 1; + + public static final int TOP = 1; + public static final int BOTTOM = 2; + public static final int LEFT = 4; + public static final int RIGHT = 8; + + /** + * Bit code indicating that the leader's location is inside the rectangle. Used by {@link + * #computeLeaderLocationCode(float, float, float, float, float, float)}. + */ + protected static final int LEADER_LOCATION_INSIDE = 0; + /** + * Bit code indicating that the leader's location is above the rectangle. Used by {@link + * #computeLeaderLocationCode(float, float, float, float, float, float)}. + */ + protected static final int LEADER_LOCATION_TOP = 1; + /** + * Bit code indicating that the leader's location is below the rectangle. Used by {@link + * #computeLeaderLocationCode(float, float, float, float, float, float)}. + */ + protected static final int LEADER_LOCATION_BOTTOM = 2; + /** + * Bit code indicating that the leader's location is to the right of the rectangle. Used by {@link + * #computeLeaderLocationCode(float, float, float, float, float, float)}. + */ + protected static final int LEADER_LOCATION_RIGHT = 4; + /** + * Bit code indicating that the leader's location is to the left of the rectangle. Used by {@link + * #computeLeaderLocationCode(float, float, float, float, float, float)}. + */ + protected static final int LEADER_LOCATION_LEFT = 8; + + private int orientation = OUTSIDE; + + public GeometryBuilder() + { + } + + public int getOrientation() + { + return this.orientation; + } + + public void setOrientation(int orientation) + { + this.orientation = orientation; + } + + //**************************************************************// + //******************** Sphere ********************************// + //**************************************************************// + + public IndexedTriangleArray tessellateSphere(float radius, int subdivisions) + { + if (radius < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "radius < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (subdivisions < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "subdivisions < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int[] indexArray = new int[ICOSAHEDRON_INDEX_COUNT]; + float[] vertexArray = new float[3 * ICOSAHEDRON_VERTEX_COUNT]; + System.arraycopy(icosahedronIndexArray, 0, indexArray, 0, ICOSAHEDRON_INDEX_COUNT); + System.arraycopy(icosahedronVertexArray, 0, vertexArray, 0, 3 * ICOSAHEDRON_VERTEX_COUNT); + + // The static icosahedron tessellation is assumed to be viewed from the outside. If the orientation is set to + // inside, then we must reverse the winding order for each triangle's indices. + if (this.orientation == INSIDE) + { + for (int index = 0; index < ICOSAHEDRON_INDEX_COUNT; index += 3) + { + int tmp = indexArray[index]; + indexArray[index] = indexArray[index + 2]; + indexArray[index + 2] = tmp; + } + } + + // Start with a triangular tessellated icosahedron. + IndexedTriangleArray ita = new IndexedTriangleArray( + ICOSAHEDRON_INDEX_COUNT, indexArray, ICOSAHEDRON_VERTEX_COUNT, vertexArray); + + // Subdivide the icosahedron a specified number of times. The subdivison step computes midpoints between + // adjacent vertices. These midpoints are not on the sphere, but must be moved onto the sphere. We normalize + // each midpoint vertex to acheive this. + for (int i = 0; i < subdivisions; i++) + { + this.subdivideIndexedTriangleArray(ita); + + vertexArray = ita.getVertices(); + for (int vertex = 0; vertex < ita.vertexCount; vertex++) + { + norm3AndSet(vertexArray, 3 * vertex); + } + } + + // Scale each vertex by the specified radius. + if (radius != 1) + { + vertexArray = ita.getVertices(); + for (int vertex = 0; vertex < ita.vertexCount; vertex++) + { + mul3AndSet(vertexArray, 3 * vertex, radius); + } + } + + return ita; + } + + public IndexedTriangleBuffer tessellateSphereBuffer(float radius, int subdivisions) + { + if (radius < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "radius < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (subdivisions < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "subdivisions < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + IntBuffer indexBuffer = BufferUtil.newIntBuffer(ICOSAHEDRON_INDEX_COUNT); + FloatBuffer vertexBuffer = BufferUtil.newFloatBuffer(3 * ICOSAHEDRON_VERTEX_COUNT); + indexBuffer.put(icosahedronIndexArray, 0, ICOSAHEDRON_INDEX_COUNT); + vertexBuffer.put(icosahedronVertexArray, 0, 3 * ICOSAHEDRON_VERTEX_COUNT); + + // The static icosahedron tessellation is assumed to be viewed from the outside. If the orientation is set to + // inside, then we must reverse the winding order for each triangle's indices. + if (this.orientation == INSIDE) + { + for (int index = 0; index < ICOSAHEDRON_INDEX_COUNT; index += 3) + { + int tmp = indexBuffer.get(index); + indexBuffer.put(index, indexBuffer.get(index + 2)); + indexBuffer.put(index + 2, tmp); + } + } + + // Start with a triangular tessellated icosahedron. + IndexedTriangleBuffer itb = new IndexedTriangleBuffer( + ICOSAHEDRON_INDEX_COUNT, indexBuffer, ICOSAHEDRON_VERTEX_COUNT, vertexBuffer); + + // Subdivide the icosahedron a specified number of times. The subdivison step computes midpoints between + // adjacent vertices. These midpoints are not on the sphere, but must be moved onto the sphere. We normalize + // each midpoint vertex to achieve this. + for (int i = 0; i < subdivisions; i++) + { + this.subdivideIndexedTriangleBuffer(itb); + + vertexBuffer = itb.getVertices(); + for (int vertex = 0; vertex < itb.getVertexCount(); vertex++) + { + norm3AndSet(vertexBuffer, 3 * vertex); + } + } + + // Scale each vertex by the specified radius. + if (radius != 1) + { + vertexBuffer = itb.getVertices(); + for (int vertex = 0; vertex < itb.vertexCount; vertex++) + { + mul3AndSet(vertexBuffer, 3 * vertex, radius); + } + } + + itb.vertices.rewind(); + itb.indices.rewind(); + + return itb; + } + + public IndexedTriangleBuffer tessellateEllipsoidBuffer(float a, float b, float c, int subdivisions) + { + IndexedTriangleBuffer itb = tessellateSphereBuffer(a, subdivisions); + + // normalize 2nd and 3rd radii in terms of the first one + float bScale = b / a; + float cScale = c / a; + + // scale Y and Z components of each vertex by appropriate scaling factor + FloatBuffer vertexBuffer = itb.getVertices(); + for (int vertex = 0; vertex < itb.getVertexCount(); vertex++) + { + // offset = 0 for x coord, 1 for y coord, etc. + mulAndSet(vertexBuffer, 3 * vertex, bScale, 2); + mulAndSet(vertexBuffer, 3 * vertex, cScale, 1); + } + + itb.vertices.rewind(); + + return itb; + } + + // Icosahedron tessellation taken from the + // OpenGL Programming Guide, Chapter 2, Example 2-13: Drawing an Icosahedron. + + private static final int ICOSAHEDRON_INDEX_COUNT = 60; + private static final int ICOSAHEDRON_VERTEX_COUNT = 12; + private static final float X = 0.525731112119133606f; + private static final float Z = 0.850650808352039932f; + + private static float[] icosahedronVertexArray = + { + -X, 0, Z, + X, 0, Z, + -X, 0, -Z, + X, 0, -Z, + 0, Z, X, + 0, Z, -X, + 0, -Z, X, + 0, -Z, -X, + Z, X, 0, + -Z, X, 0, + Z, -X, 0, + -Z, -X, 0 + }; + + private static int[] icosahedronIndexArray = + { + 1, 4, 0, + 4, 9, 0, + 4, 5, 9, + 8, 5, 4, + 1, 8, 4, + 1, 10, 8, + 10, 3, 8, + 8, 3, 5, + 3, 2, 5, + 3, 7, 2, + 3, 10, 7, + 10, 6, 7, + 6, 11, 7, + 6, 0, 11, + 6, 1, 0, + 10, 1, 6, + 11, 0, 9, + 2, 11, 9, + 5, 2, 9, + 11, 2, 7 + }; + + //**************************************************************// + //*********************** Box ********************************// + //**************************************************************// + + // create the entire box + public IndexedTriangleBuffer tessellateBoxBuffer(float radius, int subdivisions) + { + if (radius < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "radius < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (subdivisions < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "subdivisions < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + IntBuffer indexBuffer = BufferUtil.newIntBuffer(BOX_INDEX_COUNT); + FloatBuffer vertexBuffer = BufferUtil.newFloatBuffer(3 * BOX_VERTEX_COUNT); + indexBuffer.put(boxIndexArray, 0, BOX_INDEX_COUNT); + vertexBuffer.put(boxVertexArray, 0, 3 * BOX_VERTEX_COUNT); + + // The static box tessellation is assumed to be viewed from the outside. If the orientation is set to + // inside, then we must reverse the winding order for each triangle's indices. + if (this.orientation == INSIDE) + { + for (int index = 0; index < BOX_INDEX_COUNT; index += 3) + { + int tmp = indexBuffer.get(index); + indexBuffer.put(index, indexBuffer.get(index + 2)); + indexBuffer.put(index + 2, tmp); + } + } + + // Start with a tessellated box. + IndexedTriangleBuffer itb = new IndexedTriangleBuffer( + BOX_INDEX_COUNT, indexBuffer, BOX_VERTEX_COUNT, vertexBuffer); + + // Scale each vertex by the specified radius. + if (radius != 1) + { + vertexBuffer = itb.getVertices(); + for (int vertex = 0; vertex < itb.vertexCount; vertex++) + { + mul3AndSet(vertexBuffer, 3 * vertex, radius); + } + } + + itb.vertices.rewind(); + itb.indices.rewind(); + + return itb; + } + + // create only one face of the box + public IndexedTriangleBuffer tessellateBoxBuffer(int face, float radius, int subdivisions) + { + if (radius < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "radius < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (subdivisions < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "subdivisions < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (face < 0 || face >= 6) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "face < 0 or face >= 6"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + IntBuffer indexBuffer = BufferUtil.newIntBuffer(BOX_INDEX_COUNT / 6); + FloatBuffer vertexBuffer = BufferUtil.newFloatBuffer(3 * BOX_VERTEX_COUNT / 6); + + // fill subset of index buffer + int[] subArray = new int[BOX_INDEX_COUNT / 6]; + for (int i = 0; i < BOX_INDEX_COUNT / 6; i++) + { + subArray[i] = boxFacesIndexArray[face * BOX_INDEX_COUNT / 6 + i]; + } + indexBuffer.put(subArray, 0, BOX_INDEX_COUNT / 6); + + float[] vertexSubset = new float[3 * BOX_VERTEX_COUNT / 6]; + for (int i = 0; i < 3 * BOX_VERTEX_COUNT / 6; i++) + { + vertexSubset[i] = boxVertexArray[face * 3 * BOX_VERTEX_COUNT / 6 + i]; + } + vertexBuffer.put(vertexSubset, 0, 3 * BOX_VERTEX_COUNT / 6); + + // The static box tessellation is assumed to be viewed from the outside. If the orientation is set to + // inside, then we must reverse the winding order for each triangle's indices. + if (this.orientation == INSIDE) + { + for (int index = 0; index < BOX_INDEX_COUNT / 6; index += 3) + { + int tmp = indexBuffer.get(index); + indexBuffer.put(index, indexBuffer.get(index + 2)); + indexBuffer.put(index + 2, tmp); + } + } + + // Start with a tessellated box. + IndexedTriangleBuffer itb = new IndexedTriangleBuffer( + BOX_INDEX_COUNT / 6, indexBuffer, BOX_VERTEX_COUNT / 6, vertexBuffer); + + // Scale each vertex by the specified radius. + if (radius != 1) + { + vertexBuffer = itb.getVertices(); + for (int vertex = 0; vertex < itb.vertexCount; vertex++) + { + mul3AndSet(vertexBuffer, 3 * vertex, radius); + } + } + + itb.vertices.rewind(); + itb.indices.rewind(); + + return itb; + } + + private static final int BOX_INDEX_COUNT = 36; + private static final int BOX_VERTEX_COUNT = 24; + private static final float B = 1.0f; + + private static float[] boxVertexArray = + { // right + B, -B, B, // 0 + B, B, B, // 1 + B, -B, -B, // 2 + B, B, -B, // 3 + + // front + -B, B, B, // 4 + B, B, B, // 5 + -B, -B, B, // 6 + B, -B, B, // 7 + + // left + -B, B, B, // 8 + -B, -B, B, // 9 + -B, B, -B, // 10 + -B, -B, -B, // 11 + + // back + B, B, -B, // 12 + -B, B, -B, // 13 + B, -B, -B, // 14 + -B, -B, -B, // 15 + + // top + B, B, B, // 16 + -B, B, B, // 17 + B, B, -B, // 18 + -B, B, -B, // 19 + + // bottom + -B, -B, B, // 20 + B, -B, B, // 21 + -B, -B, -B, // 22 + B, -B, -B // 23 + }; + + private static int[] boxIndexArray = + { + 2, 3, 1, // right + 2, 1, 0, + 4, 6, 7, // front + 4, 7, 5, + 8, 10, 11, // left + 8, 11, 9, + 12, 14, 15, // back + 12, 15, 13, + 16, 18, 19, // top + 16, 19, 17, + 20, 22, 23, // bottom + 20, 23, 21, + }; + + private static int[] boxFacesIndexArray = + { + 2, 3, 1, // right + 2, 1, 0, + 0, 2, 3, // front + 0, 3, 1, + 0, 2, 3, // left + 0, 3, 1, + 0, 2, 3, // back + 0, 3, 1, + 0, 2, 3, // top + 0, 3, 1, + 0, 2, 3, // bottom + 0, 3, 1, + }; + + //**************************************************************// + //*********************** Pyramid ****************************// + //**************************************************************// + + public IndexedTriangleBuffer tessellatePyramidBuffer(float radius, int subdivisions) + { + if (radius < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "radius < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (subdivisions < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "subdivisions < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + IntBuffer indexBuffer = BufferUtil.newIntBuffer(PYRAMID_INDEX_COUNT); + FloatBuffer vertexBuffer = BufferUtil.newFloatBuffer(3 * PYRAMID_VERTEX_COUNT); + indexBuffer.put(pyramidIndexArray, 0, PYRAMID_INDEX_COUNT); + vertexBuffer.put(pyramidVertexArray, 0, 3 * PYRAMID_VERTEX_COUNT); + + // The static box tessellation is assumed to be viewed from the outside. If the orientation is set to + // inside, then we must reverse the winding order for each triangle's indices. + if (this.orientation == INSIDE) + { + for (int index = 0; index < PYRAMID_INDEX_COUNT; index += 3) + { + int tmp = indexBuffer.get(index); + indexBuffer.put(index, indexBuffer.get(index + 2)); + indexBuffer.put(index + 2, tmp); + } + } + + // Start with a tessellated pyramid. + IndexedTriangleBuffer itb = new IndexedTriangleBuffer( + PYRAMID_INDEX_COUNT, indexBuffer, PYRAMID_VERTEX_COUNT, vertexBuffer); + + // Scale each vertex by the specified radius. + if (radius != 1) + { + vertexBuffer = itb.getVertices(); + for (int vertex = 0; vertex < itb.vertexCount; vertex++) + { + mul3AndSet(vertexBuffer, 3 * vertex, radius); + } + } + + itb.vertices.rewind(); + itb.indices.rewind(); + + return itb; + } + + public IndexedTriangleBuffer tessellatePyramidBuffer(int face, float radius, int subdivisions) + { + if (radius < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "radius < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (subdivisions < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "subdivisions < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // default values for pyramid side + int faceIndexCount = 3; + int faceVertexCount = 3; + int faceIndicesOffset = face * faceIndexCount; + int faceVerticesOffset = face * 3 * faceVertexCount; + + if (face == 4) // the pyramid base + { + faceIndicesOffset = 4 * faceIndexCount; + faceVerticesOffset = 4 * 3 * faceVertexCount; + faceIndexCount = 6; + faceVertexCount = 4; + } + + IntBuffer indexBuffer = BufferUtil.newIntBuffer(faceIndexCount); + FloatBuffer vertexBuffer = BufferUtil.newFloatBuffer(3 * faceVertexCount); + + // fill subset of index buffer + int[] subArray = new int[faceIndexCount]; + for (int i = 0; i < faceIndexCount; i++) + { + subArray[i] = pyramidFacesIndexArray[faceIndicesOffset + i]; + } + indexBuffer.put(subArray, 0, faceIndexCount); + + float[] vertexSubset = new float[3 * faceVertexCount]; + for (int i = 0; i < 3 * faceVertexCount; i++) + { + vertexSubset[i] = pyramidVertexArray[faceVerticesOffset + i]; + } + vertexBuffer.put(vertexSubset, 0, 3 * faceVertexCount); + + // The static box tessellation is assumed to be viewed from the outside. If the orientation is set to + // inside, then we must reverse the winding order for each triangle's indices. + if (this.orientation == INSIDE) + { + for (int index = 0; index < faceIndexCount; index += 3) + { + int tmp = indexBuffer.get(index); + indexBuffer.put(index, indexBuffer.get(index + 2)); + indexBuffer.put(index + 2, tmp); + } + } + + // Start with a tessellated pyramid. + IndexedTriangleBuffer itb = new IndexedTriangleBuffer( + faceIndexCount, indexBuffer, faceVertexCount, vertexBuffer); + + // Scale each vertex by the specified radius. + if (radius != 1) + { + vertexBuffer = itb.getVertices(); + for (int vertex = 0; vertex < itb.vertexCount; vertex++) + { + mul3AndSet(vertexBuffer, 3 * vertex, radius); + } + } + + itb.vertices.rewind(); + itb.indices.rewind(); + + return itb; + } + + private static final int PYRAMID_INDEX_COUNT = 18; + private static final int PYRAMID_VERTEX_COUNT = 16; + private static final float P = 1.0f; + + private static float[] pyramidVertexArray = + { // right + 0, 0, P, // 0 (point) + P, -P, -P, // 1 + P, P, -P, // 2 + + // front + 0, 0, P, // 3 (point) + -P, -P, -P, // 4 + P, -P, -P, // 5 + + // left + 0, 0, P, // 6 (point) + -P, P, -P, // 7 + -P, -P, -P, // 8 + + // back + 0, 0, P, // 9 (point) + P, P, -P, // 10 + -P, P, -P, // 11 + + // bottom (base) face + P, P, -P, // 12 + -P, P, -P, // 13 + P, -P, -P, // 14 + -P, -P, -P // 15 + }; + + private static int[] pyramidIndexArray = + { + 0, 1, 2, // right + 3, 4, 5, // front + 6, 7, 8, // left + 9, 10, 11, // back + 12, 14, 15, // base + 12, 15, 13, + }; + + private static int[] pyramidFacesIndexArray = + { + 0, 1, 2, // right + 0, 1, 2, // front + 0, 1, 2, // left + 0, 1, 2, // back + 0, 2, 3, // base + 0, 3, 1, + }; + + //**************************************************************// + //******************** Unit Cylinder *******************// + //**************************************************************// + + public IndexedTriangleBuffer tessellateCylinderBuffer(float radius, int subdivisions) + { + if (radius < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "radius < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (subdivisions < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "subdivisions < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int i, index; + float x, y, z, a; + + int slices = (int) Math.pow(2, 2 + subdivisions); + float da = 2.0f * (float) Math.PI / (float) slices; + + int cylinderIndexCount = 12 * slices; + int cylinderVertexCount = 4 * slices + 4; + + IntBuffer indexBuffer = BufferUtil.newIntBuffer(cylinderIndexCount); + FloatBuffer vertexBuffer = BufferUtil.newFloatBuffer(3 * cylinderVertexCount); + + // VERTICES + + // top and bottom center points + vertexBuffer.put(0, 0f); + vertexBuffer.put(1, 0f); + vertexBuffer.put(2, 1.0f); + + vertexBuffer.put(3 * (slices + 1), 0f); + vertexBuffer.put(3 * (slices + 1) + 1, 0f); + vertexBuffer.put(3 * (slices + 1) + 2, -1.0f); + + // rim points + for (i = 0; i < slices; i++) + { + a = i * da; + x = (float) Math.sin(a); + y = (float) Math.cos(a); + z = 1.0f; + + index = 3 * i + 3; + + // cylinder top + vertexBuffer.put(index, x * radius); + vertexBuffer.put(index + 1, y * radius); + vertexBuffer.put(index + 2, z); + + index += 3; // add 3 for the second center point + + // cylinder bottom + vertexBuffer.put(index + 3 * slices, x * radius); + vertexBuffer.put(index + 3 * slices + 1, y * radius); + vertexBuffer.put(index + 3 * slices + 2, -z); + + index += 6 * slices + 3 * i; + + // core upper rim + vertexBuffer.put(index, x * radius); + vertexBuffer.put(index + 1, y * radius); + vertexBuffer.put(index + 2, z); + + // core lower rim + vertexBuffer.put(index + 3, x * radius); + vertexBuffer.put(index + 4, y * radius); + vertexBuffer.put(index + 5, -z); + } + + // extra vertices for seamless texture mapping + + int wrapIndex = 3 * (4 * slices + 2); + x = (float) Math.sin(0); + y = (float) Math.cos(0); + z = 1.0f; + + vertexBuffer.put(wrapIndex, x * radius); + vertexBuffer.put(wrapIndex + 1, y * radius); + vertexBuffer.put(wrapIndex + 2, z); + + vertexBuffer.put(wrapIndex + 3, x * radius); + vertexBuffer.put(wrapIndex + 4, y * radius); + vertexBuffer.put(wrapIndex + 5, -z); + + // INDICES + + int coreIndex = (2 * slices) + 2; + int centerPoint = 0; + + for (i = 0; i < slices; i++) + { + // cylinder top + index = 3 * i; + + indexBuffer.put(index, 0); // center point + indexBuffer.put(index + 1, (i < slices - 1) ? i + 2 : 1); + indexBuffer.put(index + 2, i + 1); + + // cylinder bottom + index = 3 * (slices + i); + + indexBuffer.put(index, (slices + 1)); // center point + indexBuffer.put(index + 1, (i < slices - 1) ? (slices + 1) + i + 2 : (slices + 1) + 1); + indexBuffer.put(index + 2, (slices + 1) + i + 1); + + // cylinder core + index = 6 * (slices + i); + + indexBuffer.put(index, coreIndex); + indexBuffer.put(index + 1, coreIndex + 1); + indexBuffer.put(index + 2, coreIndex + 2); + + indexBuffer.put(index + 3, coreIndex + 2); + indexBuffer.put(index + 4, coreIndex + 1); + indexBuffer.put(index + 5, coreIndex + 3); + + coreIndex += 2; + } + + // The static cylinder tessellation is assumed to be viewed from the outside. If the orientation is set to + // inside, then we must reverse the winding order for each triangle's indices. + if (this.orientation == INSIDE) + { + for (index = 0; index < cylinderIndexCount; index += 3) + { + int tmp = indexBuffer.get(index); + indexBuffer.put(index, indexBuffer.get(index + 2)); + indexBuffer.put(index + 2, tmp); + } + } + + // Start with a triangular tessellated cylinder. + IndexedTriangleBuffer itb = new IndexedTriangleBuffer( + cylinderIndexCount, indexBuffer, cylinderVertexCount, vertexBuffer); + + // Scale each vertex by the specified radius. + if (radius != 1) + { + vertexBuffer = itb.getVertices(); + for (int vertex = 0; vertex < itb.vertexCount; vertex++) + { + mul3AndSet(vertexBuffer, 3 * vertex, radius); + } + } + + itb.vertices.rewind(); + itb.indices.rewind(); + + return itb; + } + + public IndexedTriangleBuffer tessellateCylinderBuffer(int face, float radius, int subdivisions) + { + if (radius < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "radius < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (subdivisions < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "subdivisions < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int i, index; + float x, y, z, a; + + // face 0 = top + // face 1 = bottom + // face 2 = round cylinder core + + int slices = (int) Math.pow(2, 2 + subdivisions); + float da = 2.0f * (float) Math.PI / (float) slices; + + int cylinderIndexCount = 3 * slices; + int cylinderVertexCount = slices + 1; + + if (face == 2) // cylinder core + { + cylinderIndexCount = 6 * slices; + cylinderVertexCount = 2 * slices + 2; + } + + IntBuffer indexBuffer = BufferUtil.newIntBuffer(cylinderIndexCount); + FloatBuffer vertexBuffer = BufferUtil.newFloatBuffer(3 * cylinderVertexCount); + + // VERTICES + + if (face == 0 || face == 1) // top or bottom cylinder face + { + int isTop = 1; + if (face == 1) + isTop = -1; + + // top center point + vertexBuffer.put(0, 0f); + vertexBuffer.put(1, 0f); + vertexBuffer.put(2, isTop * 1.0f); + + // rim points + for (i = 0; i < slices; i++) + { + a = i * da; + x = (float) Math.sin(a); + y = (float) Math.cos(a); + z = 1.0f; + + index = 3 * i + 3; + + // cylinder top + vertexBuffer.put(index, x * radius); + vertexBuffer.put(index + 1, y * radius); + vertexBuffer.put(index + 2, isTop * z); + } + } + else if (face == 2) // cylinder core + { + // rim points + for (i = 0; i < slices; i++) + { + a = i * da; + x = (float) Math.sin(a); + y = (float) Math.cos(a); + z = 1.0f; + + index = 6 * i; + + // core upper rim + vertexBuffer.put(index, x * radius); + vertexBuffer.put(index + 1, y * radius); + vertexBuffer.put(index + 2, z); + + // core lower rim + vertexBuffer.put(index + 3, x * radius); + vertexBuffer.put(index + 4, y * radius); + vertexBuffer.put(index + 5, -z); + } + + // extra vertices for seamless texture mapping + + int wrapIndex = 3 * (2 * slices); + x = (float) Math.sin(0); + y = (float) Math.cos(0); + z = 1.0f; + + vertexBuffer.put(wrapIndex, x * radius); + vertexBuffer.put(wrapIndex + 1, y * radius); + vertexBuffer.put(wrapIndex + 2, z); + + vertexBuffer.put(wrapIndex + 3, x * radius); + vertexBuffer.put(wrapIndex + 4, y * radius); + vertexBuffer.put(wrapIndex + 5, -z); + } + + // INDICES + + int centerPoint = 0; + + if (face == 0 || face == 1) // top or bottom cylinder face + { + for (i = 0; i < slices; i++) + { + index = 3 * i; + + indexBuffer.put(index, 0); // center point + indexBuffer.put(index + 1, (i < slices - 1) ? i + 2 : 1); + indexBuffer.put(index + 2, i + 1); + } + } + else if (face == 2) // cylinder core + { + int coreIndex = 0; + + for (i = 0; i < slices; i++) + { + index = 6 * i; + + indexBuffer.put(index, coreIndex); + indexBuffer.put(index + 1, coreIndex + 1); + indexBuffer.put(index + 2, coreIndex + 2); + + indexBuffer.put(index + 3, coreIndex + 2); + indexBuffer.put(index + 4, coreIndex + 1); + indexBuffer.put(index + 5, coreIndex + 3); + + coreIndex += 2; + } + } + + // The static cylinder tessellation is assumed to be viewed from the outside. If the orientation is set to + // inside, then we must reverse the winding order for each triangle's indices. + if (this.orientation == INSIDE) + { + for (index = 0; index < cylinderIndexCount; index += 3) + { + int tmp = indexBuffer.get(index); + indexBuffer.put(index, indexBuffer.get(index + 2)); + indexBuffer.put(index + 2, tmp); + } + } + + // Start with a triangular tessellated cylinder. + IndexedTriangleBuffer itb = new IndexedTriangleBuffer( + cylinderIndexCount, indexBuffer, cylinderVertexCount, vertexBuffer); + + // Scale each vertex by the specified radius. + if (radius != 1) + { + vertexBuffer = itb.getVertices(); + for (int vertex = 0; vertex < itb.vertexCount; vertex++) + { + mul3AndSet(vertexBuffer, 3 * vertex, radius); + } + } + + itb.vertices.rewind(); + itb.indices.rewind(); + + return itb; + } + + //**************************************************************// + //******************** Wedge *******************// + //**************************************************************// + + public IndexedTriangleBuffer tessellateWedgeBuffer(float radius, int subdivisions, Angle angle) + { + if (radius < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "radius < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (subdivisions < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "subdivisions < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (angle.getRadians() < 0 || angle.getRadians() > 2 * Math.PI) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "angle < 0 or angle > 2 PI"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int i, index; + float x, y, z, a; + + int slices = (int) Math.pow(2, 2 + subdivisions); + float da = (float) angle.getRadians() / slices; + + int wedgeIndexCount = 12 * slices + 12; + int wedgeVertexCount = 4 * (slices + 1) + 2 + 8; + + IntBuffer indexBuffer = BufferUtil.newIntBuffer(wedgeIndexCount); + FloatBuffer vertexBuffer = BufferUtil.newFloatBuffer(3 * wedgeVertexCount); + + // VERTICES + + // top and bottom center points + vertexBuffer.put(0, 0f); + vertexBuffer.put(1, 0f); + vertexBuffer.put(2, 1.0f); + + vertexBuffer.put(3 * (slices + 2), 0f); + vertexBuffer.put(3 * (slices + 2) + 1, 0f); + vertexBuffer.put(3 * (slices + 2) + 2, -1.0f); + + // rim points + for (i = 0; i <= slices; i++) + { + a = i * da; + x = (float) Math.sin(a); + y = (float) Math.cos(a); + z = 1.0f; + + index = 3 * i + 3; + + // wedge top + vertexBuffer.put(index, x * radius); + vertexBuffer.put(index + 1, y * radius); + vertexBuffer.put(index + 2, z); + + index += 3; // add 3 for the second center point + + // wedge bottom + vertexBuffer.put(index + 3 * (slices + 1), x * radius); + vertexBuffer.put(index + 3 * (slices + 1) + 1, y * radius); + vertexBuffer.put(index + 3 * (slices + 1) + 2, -z); + + index = 3 * (2 * slices + 4 + 2 * i); + + // core upper rim + vertexBuffer.put(index, x * radius); + vertexBuffer.put(index + 1, y * radius); + vertexBuffer.put(index + 2, z); + + // core lower rim + vertexBuffer.put(index + 3, x * radius); + vertexBuffer.put(index + 4, y * radius); + vertexBuffer.put(index + 5, -z); + } + + // wedge sides + for (i = 0; i < 2; i++) + { + x = (float) Math.sin(i * angle.getRadians()); + y = (float) Math.cos(i * angle.getRadians()); + z = 1.0f; + + index = 3 * (4 * (slices + 1 + i) + 2); + + // inner points + vertexBuffer.put(index, 0); + vertexBuffer.put(index + 1, 0); + vertexBuffer.put(index + 2, z); + + vertexBuffer.put(index + 3, 0); + vertexBuffer.put(index + 4, 0); + vertexBuffer.put(index + 5, -z); + + // outer points + vertexBuffer.put(index + 6, x * radius); + vertexBuffer.put(index + 7, y * radius); + vertexBuffer.put(index + 8, z); + + vertexBuffer.put(index + 9, x * radius); + vertexBuffer.put(index + 10, y * radius); + vertexBuffer.put(index + 11, -z); + } + + // INDICES + + int coreIndex = 2 * (slices + 1) + 2; + + for (i = 0; i < slices; i++) + { + // wedge top + index = 3 * i; + + indexBuffer.put(index, 0); // center point + indexBuffer.put(index + 1, i + 2); + indexBuffer.put(index + 2, i + 1); + + // wedge bottom + index = 3 * (slices + i); + + indexBuffer.put(index, (slices + 2)); // center point + indexBuffer.put(index + 1, (slices + 2) + i + 2); + indexBuffer.put(index + 2, (slices + 2) + i + 1); + + // wedge core + index = 6 * (slices + i); + + indexBuffer.put(index + 0, coreIndex + 0); + indexBuffer.put(index + 1, coreIndex + 1); + indexBuffer.put(index + 2, coreIndex + 2); + + indexBuffer.put(index + 3, coreIndex + 2); + indexBuffer.put(index + 4, coreIndex + 1); + indexBuffer.put(index + 5, coreIndex + 3); + + coreIndex += 2; + } + + // wedge sides + for (i = 0; i < 2; i++) + { + index = 3 * (4 * slices) + 6 * i; + coreIndex = 4 * (slices + 1) + 2 + i * 4; + + indexBuffer.put(index + 0, coreIndex + 0); + indexBuffer.put(index + 1, coreIndex + 2); + indexBuffer.put(index + 2, coreIndex + 1); + + indexBuffer.put(index + 3, coreIndex + 1); + indexBuffer.put(index + 4, coreIndex + 2); + indexBuffer.put(index + 5, coreIndex + 3); + } + + // The static wedge tessellation is assumed to be viewed from the outside. If the orientation is set to + // inside, then we must reverse the winding order for each triangle's indices. + if (this.orientation == INSIDE) + { + for (index = 0; index < wedgeIndexCount; index += 3) + { + int tmp = indexBuffer.get(index); + indexBuffer.put(index, indexBuffer.get(index + 2)); + indexBuffer.put(index + 2, tmp); + } + } + + // Start with a triangular tessellated wedge. + IndexedTriangleBuffer itb = new IndexedTriangleBuffer( + wedgeIndexCount, indexBuffer, wedgeVertexCount, vertexBuffer); + + // Scale each vertex by the specified radius. + if (radius != 1) + { + vertexBuffer = itb.getVertices(); + for (int vertex = 0; vertex < itb.vertexCount; vertex++) + { + mul3AndSet(vertexBuffer, 3 * vertex, radius); + } + } + + itb.vertices.rewind(); + itb.indices.rewind(); + + return itb; + } + + public IndexedTriangleBuffer tessellateWedgeBuffer(int face, float radius, int subdivisions, Angle angle) + { + if (radius < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "radius < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (subdivisions < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "subdivisions < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (angle.getRadians() < 0 || angle.getRadians() > 2 * Math.PI) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "angle < 0 or angle > 2 PI"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int i, index; + float x, y, z, a; + + // face 0 = top + // face 1 = bottom + // face 2 = round core wall + // face 3 = first wedge side + // face 4 = second wedge side + + int slices = (int) Math.pow(2, 2 + subdivisions); + float da = (float) angle.getRadians() / slices; + + int wedgeIndexCount = 6; + int wedgeVertexCount = 4; + + if (face == 0 || face == 1) + { + wedgeIndexCount = 3 * slices; + wedgeVertexCount = slices + 2; + } + else if (face == 2) + { + wedgeIndexCount = 6 * slices; + wedgeVertexCount = 2 * slices + 2; + } + + IntBuffer indexBuffer = BufferUtil.newIntBuffer(wedgeIndexCount); + FloatBuffer vertexBuffer = BufferUtil.newFloatBuffer(3 * wedgeVertexCount); + + // VERTICES + + if (face == 0 || face == 1) // wedge top or bottom + { + + int isTop = 1; + if (face == 1) + isTop = -1; + + // center point + vertexBuffer.put(0, 0f); + vertexBuffer.put(1, 0f); + vertexBuffer.put(2, isTop * 1.0f); + + // rim points + for (i = 0; i <= slices; i++) + { + a = i * da; + x = (float) Math.sin(a); + y = (float) Math.cos(a); + z = 1.0f; + + index = 3 * i + 3; + + // wedge top + vertexBuffer.put(index, x * radius); + vertexBuffer.put(index + 1, y * radius); + vertexBuffer.put(index + 2, isTop * z); + } + } + else if (face == 2) // round core wall + { + // rim points + for (i = 0; i <= slices; i++) + { + a = i * da; + x = (float) Math.sin(a); + y = (float) Math.cos(a); + z = 1.0f; + + index = 3 * (2 * i); + + // core upper rim + vertexBuffer.put(index, x * radius); + vertexBuffer.put(index + 1, y * radius); + vertexBuffer.put(index + 2, z); + + // core lower rim + vertexBuffer.put(index + 3, x * radius); + vertexBuffer.put(index + 4, y * radius); + vertexBuffer.put(index + 5, -z); + } + } + else if (face == 3 || face == 4) + { + // wedge side + i = face - 3; + + x = (float) Math.sin(i * angle.getRadians()); + y = (float) Math.cos(i * angle.getRadians()); + z = 1.0f; + + index = 0; + + // inner points + vertexBuffer.put(index, 0); + vertexBuffer.put(index + 1, 0); + vertexBuffer.put(index + 2, z); + + vertexBuffer.put(index + 3, 0); + vertexBuffer.put(index + 4, 0); + vertexBuffer.put(index + 5, -z); + + // outer points + vertexBuffer.put(index + 6, x * radius); + vertexBuffer.put(index + 7, y * radius); + vertexBuffer.put(index + 8, z); + + vertexBuffer.put(index + 9, x * radius); + vertexBuffer.put(index + 10, y * radius); + vertexBuffer.put(index + 11, -z); + } + + // INDICES + + if (face == 0 || face == 1) // top or bottom + { + for (i = 0; i < slices; i++) + { + // wedge top + index = 3 * i; + + indexBuffer.put(index, 0); // center point + indexBuffer.put(index + 1, i + 2); + indexBuffer.put(index + 2, i + 1); + } + } + else if (face == 2) + { + int coreIndex = 0; + + for (i = 0; i < slices; i++) + { + // wedge core + index = 6 * i; + + indexBuffer.put(index + 0, coreIndex + 0); + indexBuffer.put(index + 1, coreIndex + 1); + indexBuffer.put(index + 2, coreIndex + 2); + + indexBuffer.put(index + 3, coreIndex + 2); + indexBuffer.put(index + 4, coreIndex + 1); + indexBuffer.put(index + 5, coreIndex + 3); + + coreIndex += 2; + } + } + else if (face == 3 || face == 4) + { + // wedge side + indexBuffer.put(0, 0); + indexBuffer.put(1, 2); + indexBuffer.put(2, 1); + + indexBuffer.put(3, 1); + indexBuffer.put(4, 2); + indexBuffer.put(5, 3); + } + + // The static wedge tessellation is assumed to be viewed from the outside. If the orientation is set to + // inside, then we must reverse the winding order for each triangle's indices. + if (this.orientation == INSIDE) + { + for (index = 0; index < wedgeIndexCount; index += 3) + { + int tmp = indexBuffer.get(index); + indexBuffer.put(index, indexBuffer.get(index + 2)); + indexBuffer.put(index + 2, tmp); + } + } + + // Start with a triangular tessellated wedge. + IndexedTriangleBuffer itb = new IndexedTriangleBuffer( + wedgeIndexCount, indexBuffer, wedgeVertexCount, vertexBuffer); + + // Scale each vertex by the specified radius. + if (radius != 1) + { + vertexBuffer = itb.getVertices(); + for (int vertex = 0; vertex < itb.vertexCount; vertex++) + { + mul3AndSet(vertexBuffer, 3 * vertex, radius); + } + } + + itb.vertices.rewind(); + itb.indices.rewind(); + + return itb; + } + + //**************************************************************// + //********************* Cone *******************// + //**************************************************************// + + public IndexedTriangleBuffer tessellateConeBuffer(float radius, int subdivisions) + { + if (radius < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "radius < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (subdivisions < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "subdivisions < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int i, index; + float x, y, z, a; + + int slices = (int) Math.pow(2, 2 + subdivisions); + float da = 2.0f * (float) Math.PI / (float) slices; + + int coneIndexCount = 12 * slices; + int coneVertexCount = 4 * slices + 4; + + IntBuffer indexBuffer = BufferUtil.newIntBuffer(coneIndexCount); + FloatBuffer vertexBuffer = BufferUtil.newFloatBuffer(3 * coneVertexCount); + + // VERTICES + + // bottom center point + vertexBuffer.put(0, 0f); + vertexBuffer.put(1, 0f); + vertexBuffer.put(2, -1.0f); + + // rim points + for (i = 0; i < slices; i++) + { + a = i * da; + x = (float) Math.sin(a); + y = (float) Math.cos(a); + z = 1.0f; + + index = 3 * i + 3; + + // cone bottom + vertexBuffer.put(index, x * radius); + vertexBuffer.put(index + 1, y * radius); + vertexBuffer.put(index + 2, -z); + + index += 3 * slices + 3 * i; + + // core upper rim - all points are at same location + vertexBuffer.put(index, 0); + vertexBuffer.put(index + 1, 0); + vertexBuffer.put(index + 2, z); + + // core lower rim + vertexBuffer.put(index + 3, x * radius); + vertexBuffer.put(index + 4, y * radius); + vertexBuffer.put(index + 5, -z); + } + + // extra vertices for seamless texture mapping + + int wrapIndex = 3 * (3 * slices + 1); + x = (float) Math.sin(0); + y = (float) Math.cos(0); + z = 1.0f; + + vertexBuffer.put(wrapIndex, 0); + vertexBuffer.put(wrapIndex + 1, 0); + vertexBuffer.put(wrapIndex + 2, z); + + vertexBuffer.put(wrapIndex + 3, x * radius); + vertexBuffer.put(wrapIndex + 4, y * radius); + vertexBuffer.put(wrapIndex + 5, -z); + + // INDICES + + int coreIndex = slices + 1; + int centerPoint = 0; + + for (i = 0; i < slices; i++) + { + index = 3 * i; + + // cone bottom + indexBuffer.put(index, 0); // center point + indexBuffer.put(index + 1, (i < slices - 1) ? i + 2 : 1); + indexBuffer.put(index + 2, i + 1); + + // cone core + index += 3 * (slices + i); + + indexBuffer.put(index, coreIndex); + indexBuffer.put(index + 1, coreIndex + 1); + indexBuffer.put(index + 2, coreIndex + 2); + + indexBuffer.put(index + 3, coreIndex + 2); + indexBuffer.put(index + 4, coreIndex + 1); + indexBuffer.put(index + 5, coreIndex + 3); + + coreIndex += 2; + } + + // The static cone tessellation is assumed to be viewed from the outside. If the orientation is set to + // inside, then we must reverse the winding order for each triangle's indices. + if (this.orientation == INSIDE) + { + for (index = 0; index < coneIndexCount; index += 3) + { + int tmp = indexBuffer.get(index); + indexBuffer.put(index, indexBuffer.get(index + 2)); + indexBuffer.put(index + 2, tmp); + } + } + + // Start with a triangular tessellated cone. + IndexedTriangleBuffer itb = new IndexedTriangleBuffer( + coneIndexCount, indexBuffer, coneVertexCount, vertexBuffer); + + // Scale each vertex by the specified radius. + if (radius != 1) + { + vertexBuffer = itb.getVertices(); + for (int vertex = 0; vertex < itb.vertexCount; vertex++) + { + mul3AndSet(vertexBuffer, 3 * vertex, radius); + } + } + + itb.vertices.rewind(); + itb.indices.rewind(); + + return itb; + } + + public IndexedTriangleBuffer tessellateConeBuffer(int face, float radius, int subdivisions) + { + if (radius < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "radius < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (subdivisions < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "subdivisions < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // face 0 = base + // face 1 = core + + int i, index; + float x, y, z, a; + + int slices = (int) Math.pow(2, 2 + subdivisions); + float da = 2.0f * (float) Math.PI / (float) slices; + + int coneIndexCount = 3 * slices; + int coneVertexCount = slices + 1; + + if (face == 1) // cone core + { + coneIndexCount = 6 * slices; + coneVertexCount = 2 * slices + 2; + } + + IntBuffer indexBuffer = BufferUtil.newIntBuffer(coneIndexCount); + FloatBuffer vertexBuffer = BufferUtil.newFloatBuffer(3 * coneVertexCount); + + // VERTICES + + if (face == 0) // cone base + { + // base center point + vertexBuffer.put(0, 0f); + vertexBuffer.put(1, 0f); + vertexBuffer.put(2, -1.0f); + + // base rim points + for (i = 0; i < slices; i++) + { + a = i * da; + x = (float) Math.sin(a); + y = (float) Math.cos(a); + z = 1.0f; + + index = 3 * i + 3; + + vertexBuffer.put(index, x * radius); + vertexBuffer.put(index + 1, y * radius); + vertexBuffer.put(index + 2, -z); + } + } + else if (face == 1) // cone core + { + // rim points + for (i = 0; i < slices; i++) + { + a = i * da; + x = (float) Math.sin(a); + y = (float) Math.cos(a); + z = 1.0f; + + index = 6 * i; + + // core upper rim + vertexBuffer.put(index, 0); + vertexBuffer.put(index + 1, 0); + vertexBuffer.put(index + 2, z); + + // core lower rim + vertexBuffer.put(index + 3, x * radius); + vertexBuffer.put(index + 4, y * radius); + vertexBuffer.put(index + 5, -z); + } + + // extra vertices for seamless texture mapping + + int wrapIndex = 3 * (2 * slices); + x = (float) Math.sin(0); + y = (float) Math.cos(0); + z = 1.0f; + + vertexBuffer.put(wrapIndex, 0); + vertexBuffer.put(wrapIndex + 1, 0); + vertexBuffer.put(wrapIndex + 2, z); + + vertexBuffer.put(wrapIndex + 3, x * radius); + vertexBuffer.put(wrapIndex + 4, y * radius); + vertexBuffer.put(wrapIndex + 5, -z); + } + + // INDICES + + int centerPoint = 0; + + if (face == 0) // cone base + { + for (i = 0; i < slices; i++) + { + index = 3 * i; + + indexBuffer.put(index, 0); // center point + indexBuffer.put(index + 1, (i < slices - 1) ? i + 2 : 1); + indexBuffer.put(index + 2, i + 1); + } + } + else if (face == 1) // cone core + { + int coreIndex = 0; + + for (i = 0; i < slices; i++) + { + index = 6 * i; + + indexBuffer.put(index, coreIndex); + indexBuffer.put(index + 1, coreIndex + 1); + indexBuffer.put(index + 2, coreIndex + 2); + + indexBuffer.put(index + 3, coreIndex + 2); + indexBuffer.put(index + 4, coreIndex + 1); + indexBuffer.put(index + 5, coreIndex + 3); + + coreIndex += 2; + } + } + + // The static cone tessellation is assumed to be viewed from the outside. If the orientation is set to + // inside, then we must reverse the winding order for each triangle's indices. + if (this.orientation == INSIDE) + { + for (index = 0; index < coneIndexCount; index += 3) + { + int tmp = indexBuffer.get(index); + indexBuffer.put(index, indexBuffer.get(index + 2)); + indexBuffer.put(index + 2, tmp); + } + } + + // Start with a triangular tessellated cone. + IndexedTriangleBuffer itb = new IndexedTriangleBuffer( + coneIndexCount, indexBuffer, coneVertexCount, vertexBuffer); + + // Scale each vertex by the specified radius. + if (radius != 1) + { + vertexBuffer = itb.getVertices(); + for (int vertex = 0; vertex < itb.vertexCount; vertex++) + { + mul3AndSet(vertexBuffer, 3 * vertex, radius); + } + } + + itb.vertices.rewind(); + itb.indices.rewind(); + + return itb; + } + + //**************************************************************// + //******************** Cylinder *******************// + //**************************************************************// + + public int getCylinderVertexCount(int slices, int stacks) + { + return slices * (stacks + 1); + } + + public int getCylinderIndexCount(int slices, int stacks) + { + return stacks * 2 * (slices + 1) + 2 * (stacks - 1); + } + + @SuppressWarnings({"UnusedDeclaration"}) + public int getCylinderOutlineIndexCount(int slices, int stacks) + { + return slices * 4; + } + + public int getCylinderDrawMode() + { + return GL_TRIANGLE_STRIP; + } + + public int getCylinderOutlineDrawMode() + { + return GL_LINES; + } + + public void makeCylinderVertices(float radius, float height, int slices, int stacks, float[] dest) + { + int numPoints = this.getCylinderVertexCount(slices, stacks); + int numCoords = 3 * numPoints; + + if (numPoints < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "slices=" + slices + " stacks=" + stacks); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + float x, y, z; + float a; + float dz, da; + int i, j; + int index; + + if (stacks != 0.0f) + dz = height / (float) stacks; + else + dz = 0.0f; + da = 2.0f * (float) Math.PI / (float) slices; + + for (i = 0; i < slices; i++) + { + a = i * da; + x = (float) Math.sin(a); + y = (float) Math.cos(a); + z = 0.0f; + for (j = 0; j <= stacks; j++) + { + index = j + i * (stacks + 1); + index = 3 * index; + dest[index] = x * radius; + dest[index + 1] = y * radius; + dest[index + 2] = z; + z += dz; + } + } + } + + public void makeCylinderNormals(int slices, int stacks, float[] dest) + { + int numPoints = this.getCylinderVertexCount(slices, stacks); + int numCoords = 3 * numPoints; + + if (numPoints < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "slices=" + slices + " stacks=" + stacks); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + float x, y; + float a; + float da; + float nsign; + int i, j; + int index; + float[] norm; + + da = 2.0f * (float) Math.PI / (float) slices; + nsign = (this.orientation == OUTSIDE) ? 1.0f : -1.0f; + norm = new float[3]; + + for (i = 0; i < slices; i++) + { + a = i * da; + x = (float) Math.sin(a); + y = (float) Math.cos(a); + norm[0] = x * nsign; + norm[1] = y * nsign; + norm[2] = 0.0f; + this.norm3AndSet(norm, 0); + + for (j = 0; j <= stacks; j++) + { + index = j + i * (stacks + 1); + index = 3 * index; + System.arraycopy(norm, 0, dest, index, 3); + } + } + } + + public void makeCylinderIndices(int slices, int stacks, int[] dest) + { + int numIndices = this.getCylinderIndexCount(slices, stacks); + + if (numIndices < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "slices=" + slices + " stacks=" + stacks); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numIndices) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int i, j; + int vertex, index; + + index = 0; + for (j = 0; j < stacks; j++) + { + if (j != 0) + { + if (this.orientation == INSIDE) + vertex = j + 1; + else // (this.orientation == OUTSIDE) + vertex = j; + dest[index++] = vertex; + dest[index++] = vertex; + } + for (i = 0; i <= slices; i++) + { + if (i == slices) + vertex = j; + else + vertex = j + i * (stacks + 1); + if (this.orientation == INSIDE) + { + dest[index++] = vertex + 1; + dest[index++] = vertex; + } + else // (this.orientation == OUTSIDE) + { + dest[index++] = vertex; + dest[index++] = vertex + 1; + } + } + } + } + + public void makeCylinderOutlineIndices(int slices, int stacks, int[] dest) + { + int numIndices = this.getCylinderOutlineIndexCount(slices, stacks); + + if (numIndices < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "slices=" + slices + " stacks=" + stacks); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numIndices) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int i; + int vertex, index; + + index = 0; + // Bottom ring + for (i = 0; i < slices; i++) + { + vertex = i * (stacks + 1); + dest[index++] = vertex; + dest[index++] = (i != slices - 1) ? vertex + stacks + 1 : 0; + } + // Top ring + for (i = 0; i < slices; i++) + { + vertex = i * (stacks + 1) + stacks; + dest[index++] = vertex; + dest[index++] = (i != slices - 1) ? vertex + stacks + 1 : stacks; + } +// // Vertical edges +// for (i = 0; i < slices; i++) +// { +// vertex = i * (stacks + 1); +// dest[index++] = vertex; +// dest[index++] = vertex + stacks; +// } + } + + //**************************************************************// + //******************** Partial Cylinder ********************// + //**************************************************************// + + public int getPartialCylinderVertexCount(int slices, int stacks) + { + return (slices + 1) * (stacks + 1); + } + + public int getPartialCylinderIndexCount(int slices, int stacks) + { + return stacks * 2 * (slices + 1) + 2 * (stacks - 1); + } + + @SuppressWarnings({"UnusedDeclaration"}) + public int getPartialCylinderOutlineIndexCount(int slices, int stacks) + { + return slices * 4; + } + + public int getPartialCylinderDrawMode() + { + return GL_TRIANGLE_STRIP; + } + + public int getPartialCylinderOutlineDrawMode() + { + return GL_LINES; + } + + public void makePartialCylinderVertices(float radius, float height, int slices, int stacks, + float start, float sweep, float[] dest) + { + int numPoints = this.getPartialCylinderVertexCount(slices, stacks); + int numCoords = 3 * numPoints; + + if (numPoints < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "slices=" + slices + " stacks=" + stacks); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + float x, y, z; + float a; + float dz, da; + int i, j; + int index; + + if (stacks != 0.0f) + dz = height / (float) stacks; + else + dz = 0.0f; + da = sweep / (float) slices; + + for (i = 0; i <= slices; i++) + { + a = i * da + start; + x = (float) Math.sin(a); + y = (float) Math.cos(a); + z = 0.0f; + for (j = 0; j <= stacks; j++) + { + index = j + i * (stacks + 1); + index = 3 * index; + dest[index] = x * radius; + dest[index + 1] = y * radius; + dest[index + 2] = z; + z += dz; + } + } + } + + @SuppressWarnings({"UnusedDeclaration"}) + public void makePartialCylinderNormals(float radius, float height, int slices, int stacks, + float start, float sweep, float[] dest) + { + int numPoints = this.getPartialCylinderVertexCount(slices, stacks); + int numCoords = 3 * numPoints; + + if (numPoints < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "slices=" + slices + " stacks=" + stacks); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + float x, y; + float a; + float da; + float nsign; + int i, j; + int index; + float[] norm; + + da = sweep / (float) slices; + nsign = (this.orientation == OUTSIDE) ? 1.0f : -1.0f; + norm = new float[3]; + + for (i = 0; i <= slices; i++) + { + a = i * da + start; + x = (float) Math.sin(a); + y = (float) Math.cos(a); + norm[0] = x * nsign; + norm[1] = y * nsign; + norm[2] = 0.0f; + this.norm3AndSet(norm, 0); + + for (j = 0; j <= stacks; j++) + { + index = j + i * (stacks + 1); + index = 3 * index; + System.arraycopy(norm, 0, dest, index, 3); + } + } + } + + public void makePartialCylinderIndices(int slices, int stacks, int[] dest) + { + int numIndices = this.getPartialCylinderIndexCount(slices, stacks); + + if (numIndices < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "slices=" + slices + " stacks=" + stacks); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numIndices) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int i, j; + int vertex, index; + + index = 0; + for (j = 0; j < stacks; j++) + { + if (j != 0) + { + if (this.orientation == INSIDE) + { + vertex = j + slices * (stacks + 1); + dest[index++] = vertex - 1; + vertex = j + 1; + dest[index++] = vertex; + } + else //(this.orientation == OUTSIDE) + { + vertex = j + slices * (stacks + 1); + dest[index++] = vertex; + vertex = j; + dest[index++] = vertex; + } + } + for (i = 0; i <= slices; i++) + { + vertex = j + i * (stacks + 1); + if (this.orientation == INSIDE) + { + dest[index++] = vertex + 1; + dest[index++] = vertex; + } + else //(this.orientation == OUTSIDE) + { + dest[index++] = vertex; + dest[index++] = vertex + 1; + } + } + } + } + + public void makePartialCylinderOutlineIndices(int slices, int stacks, int[] dest) + { + int numIndices = this.getPartialCylinderOutlineIndexCount(slices, stacks); + + if (numIndices < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "slices=" + slices + " stacks=" + stacks); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numIndices) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int i; + int vertex, index; + + index = 0; + // Bottom ring + for (i = 0; i < slices; i++) + { + vertex = i * (stacks + 1); + dest[index++] = vertex; + dest[index++] = vertex + stacks + 1; + } + // Top ring + for (i = 0; i < slices; i++) + { + vertex = i * (stacks + 1) + stacks; + dest[index++] = vertex; + dest[index++] = vertex + stacks + 1; + } + } + + //**************************************************************// + //******************** Disk ********************// + //**************************************************************// + + public int getDiskVertexCount(int slices, int loops) + { + return slices * (loops + 1); + } + + public int getDiskIndexCount(int slices, int loops) + { + return loops * 2 * (slices + 1) + 2 * (loops - 1); + } + + public int getDiskDrawMode() + { + return GL_TRIANGLE_STRIP; + } + + public void makeDiskVertices(float innerRadius, float outerRadius, int slices, int loops, float[] dest) + { + int numPoints = this.getDiskVertexCount(slices, loops); + int numCoords = 3 * numPoints; + + if (numPoints < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "slices=" + slices + " loops=" + loops); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + float x, y; + float a, r; + float da, dr; + int s, l; + int index; + + da = 2.0f * (float) Math.PI / (float) slices; + dr = (outerRadius - innerRadius) / (float) loops; + + for (s = 0; s < slices; s++) + { + a = s * da; + x = (float) Math.sin(a); + y = (float) Math.cos(a); + for (l = 0; l <= loops; l++) + { + index = l + s * (loops + 1); + index = 3 * index; + r = innerRadius + l * dr; + dest[index] = r * x; + dest[index + 1] = r * y; + dest[index + 2] = 0.0f; + } + } + } + + public void makeDiskNormals(int slices, int loops, float[] dest) + { + int numPoints = this.getDiskVertexCount(slices, loops); + int numCoords = 3 * numPoints; + + if (numPoints < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "slices=" + slices + " loops=" + loops); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int s, l; + int index; + float nsign; + float[] normal; + + nsign = (this.orientation == OUTSIDE) ? 1.0f : -1.0f; + normal = new float[3]; + normal[0] = 0.0f; + normal[1] = 0.0f; + //noinspection PointlessArithmeticExpression + normal[2] = 1.0f * nsign; + + for (s = 0; s < slices; s++) + { + for (l = 0; l <= loops; l++) + { + index = l + s * (loops + 1); + index = 3 * index; + System.arraycopy(normal, 0, dest, index, 3); + } + } + } + + @SuppressWarnings({"UnusedDeclaration"}) + public void makeDiskVertexNormals(float innerRadius, float outerRadius, int slices, int loops, + float[] srcVerts, float[] dest) + { + int numPoints = this.getDiskVertexCount(slices, loops); + int numCoords = 3 * numPoints; + + if (numPoints < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "slices=" + slices + " loops=" + loops); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (srcVerts == null) + { + String message = "nullValue.SourceVertexArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int s, l; + int index; + float nsign; + float[] norm, zero, tmp; + + nsign = (this.orientation == OUTSIDE) ? 1.0f : -1.0f; + norm = new float[3]; + zero = new float[3]; + tmp = new float[3]; + + for (l = 0; l <= loops; l++) + { + // Normal vectors for first and last loops require a special case. + if (l == 0 || l == loops) + { + // Closed disk: all slices share a common center point. + if (l == 0 && innerRadius == 0.0f) + { + // Compute common center point normal. + int nextSlice; + int adjacentLoop; + System.arraycopy(zero, 0, norm, 0, 3); + for (s = 0; s < slices; s++) + { + index = l + s * (loops + 1); + nextSlice = l + (s + 1) * (loops + 1); + if (s == slices - 1) + nextSlice = l; + adjacentLoop = index + 1; + this.facenorm(srcVerts, index, nextSlice + 1, adjacentLoop, tmp); + this.add3AndSet(norm, 0, tmp, 0); + } + this.mul3AndSet(norm, 0, nsign); + this.norm3AndSet(norm, 0); + // Copy common normal to the first point of each slice. + for (s = 0; s < slices; s++) + { + index = l + s * (loops + 1); + System.arraycopy(norm, 0, dest, 3 * index, 3); + } + } + // Open disk: each slice has a unique starting point. + else + { + for (s = 0; s < slices; s++) + { + int prevSlice, nextSlice; + int adjacentLoop; + index = l + s * (loops + 1); + prevSlice = l + (s - 1) * (loops + 1); + nextSlice = l + (s + 1) * (loops + 1); + + if (s == 0) + prevSlice = l + (slices - 1) * (loops + 1); + else if (s == slices - 1) + nextSlice = l; + + if (l == 0) + adjacentLoop = index + 1; + else + adjacentLoop = index - 1; + + System.arraycopy(zero, 0, norm, 0, 3); + + // Add clockwise adjacent face. + if (l == 0) + this.facenorm(srcVerts, index, nextSlice, adjacentLoop, tmp); + else + this.facenorm(srcVerts, index, adjacentLoop, nextSlice, tmp); + this.add3AndSet(norm, 0, tmp, 0); + // Add counter-clockwise adjacent face. + if (l == 0) + this.facenorm(srcVerts, index, adjacentLoop, prevSlice, tmp); + else + this.facenorm(srcVerts, index, prevSlice, adjacentLoop, tmp); + this.add3AndSet(norm, 0, tmp, 0); + + // Normalize and place in output. + this.mul3AndSet(norm, 0, nsign); + this.norm3AndSet(norm, 0); + System.arraycopy(norm, 0, dest, 3 * index, 3); + } + } + } + // Normal vectors for internal loops. + else + { + for (s = 0; s < slices; s++) + { + int prevSlice, nextSlice; + int prevLoop, nextLoop; + index = l + s * (loops + 1); + prevSlice = l + (s - 1) * (loops + 1); + nextSlice = l + (s + 1) * (loops + 1); + + if (s == 0) + prevSlice = l + (slices - 1) * (loops + 1); + else if (s == slices - 1) + nextSlice = l; + + prevLoop = index - 1; + nextLoop = index + 1; + + System.arraycopy(zero, 0, norm, 0, 3); + + // Add lower-left adjacent face. + this.facenorm(srcVerts, index, prevSlice, prevSlice - 1, tmp); + this.add3AndSet(norm, 0, tmp, 0); + this.facenorm(srcVerts, index, prevSlice - 1, prevLoop, tmp); + this.add3AndSet(norm, 0, tmp, 0); + // Add lower-right adjacent face. + this.facenorm(srcVerts, index, prevLoop, nextSlice - 1, tmp); + this.add3AndSet(norm, 0, tmp, 0); + this.facenorm(srcVerts, index, nextSlice - 1, nextSlice, tmp); + this.add3AndSet(norm, 0, tmp, 0); + // Add upper-right adjacent face. + this.facenorm(srcVerts, index, nextSlice, nextSlice + 1, tmp); + this.add3AndSet(norm, 0, tmp, 0); + this.facenorm(srcVerts, index, nextSlice + 1, nextLoop, tmp); + this.add3AndSet(norm, 0, tmp, 0); + // Add upper-left adjacent face. + this.facenorm(srcVerts, index, nextLoop, prevSlice + 1, tmp); + this.add3AndSet(norm, 0, tmp, 0); + this.facenorm(srcVerts, index, prevSlice + 1, prevSlice, tmp); + this.add3AndSet(norm, 0, tmp, 0); + + // Normalize and place in output. + this.mul3AndSet(norm, 0, nsign); + this.norm3AndSet(norm, 0); + System.arraycopy(norm, 0, dest, 3 * index, 3); + } + } + } + } + + public void makeDiskIndices(int slices, int loops, int[] dest) + { + int numIndices = this.getDiskIndexCount(slices, loops); + + if (numIndices < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "slices=" + slices + " loops=" + loops); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numIndices) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int s, l; + int vertex, index; + + index = 0; + for (l = 0; l < loops; l++) + { + if (l != 0) + { + if (this.orientation == INSIDE) + { + vertex = l; + dest[index++] = vertex; + dest[index++] = vertex; + } + else // (this.orientation == OUTSIDE) + { + vertex = l - 1; + dest[index++] = vertex; + vertex = l + 1; + dest[index++] = vertex; + } + } + for (s = 0; s <= slices; s++) + { + if (s == slices) + vertex = l; + else + vertex = l + s * (loops + 1); + if (this.orientation == INSIDE) + { + dest[index++] = vertex; + dest[index++] = vertex + 1; + } + else // (this.orientation == OUTSIDE) + { + dest[index++] = vertex + 1; + dest[index++] = vertex; + } + } + } + } + + //**************************************************************// + //******************** Partial Disk ********************// + //**************************************************************// + + public int getPartialDiskVertexCount(int slices, int loops) + { + return (slices + 1) * (loops + 1); + } + + public int getPartialDiskIndexCount(int slices, int loops) + { + return loops * 2 * (slices + 1) + 2 * (loops - 1); + } + + public int getPartialDiskDrawMode() + { + return GL_TRIANGLE_STRIP; + } + + public void makePartialDiskVertices(float innerRadius, float outerRadius, int slices, int loops, + float start, float sweep, float[] dest) + { + int numPoints = this.getPartialDiskVertexCount(slices, loops); + int numCoords = 3 * numPoints; + + if (numPoints < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "slices=" + slices + " loops=" + loops); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + float x, y; + float a, r; + float da, dr; + int s, l; + int index; + + da = sweep / (float) slices; + dr = (outerRadius - innerRadius) / (float) loops; + + for (s = 0; s <= slices; s++) + { + a = s * da + start; + x = (float) Math.sin(a); + y = (float) Math.cos(a); + for (l = 0; l <= loops; l++) + { + index = l + s * (loops + 1); + index = 3 * index; + r = innerRadius + l * dr; + dest[index] = r * x; + dest[index + 1] = r * y; + dest[index + 2] = 0.0f; + } + } + } + + public void makePartialDiskNormals(int slices, int loops, float[] dest) + { + int numPoints = this.getPartialDiskVertexCount(slices, loops); + int numCoords = 3 * numPoints; + + if (numPoints < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "slices=" + slices + " loops=" + loops); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int s, l; + int index; + float nsign; + float[] normal; + + nsign = (this.orientation == OUTSIDE) ? 1.0f : -1.0f; + normal = new float[3]; + normal[0] = 0.0f; + normal[1] = 0.0f; + //noinspection PointlessArithmeticExpression + normal[2] = 1.0f * nsign; + + for (s = 0; s <= slices; s++) + { + for (l = 0; l <= loops; l++) + { + index = l + s * (loops + 1); + index = 3 * index; + System.arraycopy(normal, 0, dest, index, 3); + } + } + } + + @SuppressWarnings({"UnusedDeclaration"}) + public void makePartialDiskVertexNormals(float innerRadius, float outerRadius, int slices, int loops, + float start, float sweep, float[] srcVerts, float[] dest) + { + int numPoints = this.getPartialDiskVertexCount(slices, loops); + int numCoords = 3 * numPoints; + + if (numPoints < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "slices=" + slices + " loops=" + loops); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (srcVerts == null) + { + String message = "nullValue.SourceVertexArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int s, l; + int index; + float nsign; + float[] norm, zero, tmp; + + nsign = (this.orientation == OUTSIDE) ? 1.0f : -1.0f; + norm = new float[3]; + zero = new float[3]; + tmp = new float[3]; + + for (l = 0; l <= loops; l++) + { + // Normal vectors for first and last loops require a special case. + if (l == 0 || l == loops) + { + // Closed disk: all slices share a common center point. + if (l == 0 && innerRadius == 0.0f) + { + // Compute common center point normal. + int nextSlice; + int adjacentLoop; + System.arraycopy(zero, 0, norm, 0, 3); + for (s = 0; s < slices; s++) + { + index = l + s * (loops + 1); + nextSlice = l + (s + 1) * (loops + 1); + adjacentLoop = index + 1; + this.facenorm(srcVerts, index, nextSlice + 1, adjacentLoop, tmp); + this.add3AndSet(norm, 0, tmp, 0); + } + this.mul3AndSet(norm, 0, nsign); + this.norm3AndSet(norm, 0); + // Copy common normal to the first point of each slice. + for (s = 0; s <= slices; s++) + { + index = l + s * (loops + 1); + System.arraycopy(norm, 0, dest, 3 * index, 3); + } + } + // Open disk: each slice has a unique starting point. + else + { + for (s = 0; s <= slices; s++) + { + int prevSlice, nextSlice; + int adjacentLoop; + index = l + s * (loops + 1); + + if (l == 0) + adjacentLoop = index + 1; + else + adjacentLoop = index - 1; + + System.arraycopy(zero, 0, norm, 0, 3); + + if (s > 0) + { + prevSlice = l + (s - 1) * (loops + 1); + // Add counter-clockwise adjacent face. + if (l == 0) + this.facenorm(srcVerts, index, adjacentLoop, prevSlice, tmp); + else + this.facenorm(srcVerts, index, prevSlice, adjacentLoop, tmp); + this.add3AndSet(norm, 0, tmp, 0); + } + if (s < slices) + { + nextSlice = l + (s + 1) * (loops + 1); + // Add clockwise adjacent face. + if (l == 0) + this.facenorm(srcVerts, index, nextSlice, adjacentLoop, tmp); + else + this.facenorm(srcVerts, index, adjacentLoop, nextSlice, tmp); + this.add3AndSet(norm, 0, tmp, 0); + } + + // Normalize and place in output. + this.mul3AndSet(norm, 0, nsign); + this.norm3AndSet(norm, 0); + System.arraycopy(norm, 0, dest, 3 * index, 3); + } + } + } + // Normal vectors for internal loops. + else + { + for (s = 0; s <= slices; s++) + { + int prevSlice, nextSlice; + int prevLoop, nextLoop; + index = l + s * (loops + 1); + prevLoop = index - 1; + nextLoop = index + 1; + + System.arraycopy(zero, 0, norm, 0, 3); + if (s > 0) + { + prevSlice = l + (s - 1) * (loops + 1); + // Add lower-left adjacent face. + this.facenorm(srcVerts, index, prevSlice, prevSlice - 1, tmp); + this.add3AndSet(norm, 0, tmp, 0); + this.facenorm(srcVerts, index, prevSlice - 1, prevLoop, tmp); + this.add3AndSet(norm, 0, tmp, 0); + // Add upper-left adjacent face. + this.facenorm(srcVerts, index, nextLoop, prevSlice + 1, tmp); + this.add3AndSet(norm, 0, tmp, 0); + this.facenorm(srcVerts, index, prevSlice + 1, prevSlice, tmp); + this.add3AndSet(norm, 0, tmp, 0); + } + if (s < slices) + { + nextSlice = l + (s + 1) * (loops + 1); + // Add lower-right adjacent face. + this.facenorm(srcVerts, index, prevLoop, nextSlice - 1, tmp); + this.add3AndSet(norm, 0, tmp, 0); + this.facenorm(srcVerts, index, nextSlice - 1, nextSlice, tmp); + this.add3AndSet(norm, 0, tmp, 0); + // Add upper-right adjacent face. + this.facenorm(srcVerts, index, nextSlice, nextSlice + 1, tmp); + this.add3AndSet(norm, 0, tmp, 0); + this.facenorm(srcVerts, index, nextSlice + 1, nextLoop, tmp); + this.add3AndSet(norm, 0, tmp, 0); + } + + // Normalize and place in output. + this.mul3AndSet(norm, 0, nsign); + this.norm3AndSet(norm, 0); + System.arraycopy(norm, 0, dest, 3 * index, 3); + } + } + } + } + + public void makePartialDiskIndices(int slices, int loops, int[] dest) + { + int numIndices = this.getPartialDiskIndexCount(slices, loops); + + if (numIndices < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "slices=" + slices + " loops=" + loops); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numIndices) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int s, l; + int vertex, index; + + index = 0; + for (l = 0; l < loops; l++) + { + if (l != 0) + { + if (this.orientation == INSIDE) + { + vertex = l + slices * (loops + 1); + dest[index++] = vertex; + vertex = l; + dest[index++] = vertex; + } + else // (this.orientation == OUTSIDE) + { + vertex = (l - 1) + slices * (loops + 1); + dest[index++] = vertex; + vertex = l; + dest[index++] = vertex + 1; + } + } + for (s = 0; s <= slices; s++) + { + vertex = l + s * (loops + 1); + if (this.orientation == INSIDE) + { + dest[index++] = vertex; + dest[index++] = vertex + 1; + } + else // (this.orientation == OUTSIDE) + { + dest[index++] = vertex + 1; + dest[index++] = vertex; + } + } + } + } + + //**************************************************************// + //******************** Radial Wall ********************// + //**************************************************************// + + public int getRadialWallVertexCount(int pillars, int stacks) + { + return (pillars + 1) * (stacks + 1); + } + + public int getRadialWallIndexCount(int pillars, int stacks) + { + return stacks * 2 * (pillars + 1) + 2 * (stacks - 1); + } + + @SuppressWarnings({"UnusedDeclaration"}) + public int getRadialWallOutlineIndexCount(int pillars, int stacks) + { + return pillars * 4; + } + + public int getRadialWallDrawMode() + { + return GL_TRIANGLE_STRIP; + } + + public int getRadialWallOutlineDrawMode() + { + return GL_LINES; + } + + public void makeRadialWallVertices(float innerRadius, float outerRadius, float height, float angle, + int pillars, int stacks, float[] dest) + { + int numPoints = this.getRadialWallVertexCount(pillars, stacks); + int numCoords = 3 * numPoints; + + if (numPoints < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "pillars=" + pillars + + " stacks=" + stacks); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + float x, y, z; + float a, r; + float dz, dr; + int s, p; + int index; + + a = angle; + x = (float) Math.sin(a); + y = (float) Math.cos(a); + z = 0.0f; + + if (stacks != 0.0f) + dz = height / (float) stacks; + else + dz = 0.0f; + dr = (outerRadius - innerRadius) / (float) pillars; + + for (s = 0; s <= stacks; s++) + { + for (p = 0; p <= pillars; p++) + { + index = p + s * (pillars + 1); + index = 3 * index; + r = innerRadius + p * dr; + dest[index] = r * x; + dest[index + 1] = r * y; + dest[index + 2] = z; + } + z += dz; + } + } + + @SuppressWarnings({"UnusedDeclaration"}) + public void makeRadialWallNormals(float innerRadius, float outerRadius, float height, float angle, + int pillars, int stacks, float[] dest) + { + int numPoints = this.getRadialWallVertexCount(pillars, stacks); + int numCoords = 3 * numPoints; + + if (numPoints < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "pillars=" + pillars + + " stacks=" + stacks); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + float x, y; + float a; + int s, p; + int index; + float nsign; + float[] norm; + + a = angle; + x = (float) Math.cos(a); + y = (float) -Math.sin(a); + + nsign = (this.orientation == OUTSIDE) ? 1.0f : -1.0f; + norm = new float[3]; + norm[0] = x * nsign; + norm[1] = y * nsign; + norm[2] = 0.0f; + this.norm3AndSet(norm, 0); + + for (s = 0; s <= stacks; s++) + { + for (p = 0; p <= pillars; p++) + { + index = p + s * (pillars + 1); + index = 3 * index; + System.arraycopy(norm, 0, dest, index, 3); + } + } + } + + public void makeRadialWallIndices(int pillars, int stacks, int[] dest) + { + int numIndices = this.getRadialWallIndexCount(pillars, stacks); + + if (numIndices < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "pillars=" + pillars + + " stacks=" + stacks); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numIndices) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int p, s; + int vertex, index; + + index = 0; + for (s = 0; s < stacks; s++) + { + if (s != 0) + { + if (this.orientation == INSIDE) + { + vertex = pillars + s * (pillars + 1); + dest[index++] = vertex; + vertex = s * (pillars + 1); + dest[index++] = vertex; + } + else // (this.orientation == OUTSIDE) + { + vertex = pillars + (s - 1) * (pillars + 1); + dest[index++] = vertex; + vertex = (s + 1) * (pillars + 1); + dest[index++] = vertex; + } + } + for (p = 0; p <= pillars; p++) + { + vertex = p + s * (pillars + 1); + if (this.orientation == INSIDE) + { + dest[index++] = vertex; + dest[index++] = vertex + (pillars + 1); + } + else // (this.orientation == OUTSIDE) + { + dest[index++] = vertex + (pillars + 1); + dest[index++] = vertex; + } + } + } + } + + public void makeRadialWallOutlineIndices(int pillars, int stacks, int[] dest) + { + int numIndices = this.getRadialWallOutlineIndexCount(pillars, stacks); + + if (numIndices < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "pillars=" + pillars + + " stacks=" + stacks); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numIndices) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int vertex; + int index = 0; + // Bottom + for (int i = 0; i < pillars; i++) + { + vertex = i; + dest[index++] = vertex; + dest[index++] = vertex + 1; + } + // Top + for (int i = 0; i < pillars; i++) + { + vertex = i + stacks * (pillars + 1); + dest[index++] = vertex; + dest[index++] = vertex + 1; + } + } + + //**************************************************************// + //******************** Long Cylinder ********************// + //**************************************************************// + + public int getLongCylinderVertexCount(int arcSlices, int lengthSlices, int stacks) + { + int slices = 2 * (arcSlices + 1) + 2 * (lengthSlices - 1); + return slices * (stacks + 1); + } + + public int getLongCylinderIndexCount(int arcSlices, int lengthSlices, int stacks) + { + int slices = 2 * (arcSlices + 1) + 2 * (lengthSlices - 1); + return stacks * 2 * (slices + 1) + 2 * (stacks - 1); + } + + @SuppressWarnings({"UnusedDeclaration"}) + public int getLongCylinderOutlineIndexCount(int arcSlices, int lengthSlices, int stacks) + { + return (arcSlices + lengthSlices) * 2 * 4; + } + + public int getLongCylinderDrawMode() + { + return GL_TRIANGLE_STRIP; + } + + public int getLongCylinderOutlineDrawMode() + { + return GL_LINES; + } + + public void makeLongCylinderVertices(float radius, float length, float height, + int arcSlices, int lengthSlices, int stacks, float[] dest) + { + int numPoints = this.getLongCylinderVertexCount(arcSlices, lengthSlices, stacks); + int numCoords = 3 * numPoints; + + if (numPoints < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "arcSlices=" + arcSlices + + " lengthSlices=" + lengthSlices + " stacks=" + stacks); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + float x, y, z; + float a; + float dy, dz, da; + int i, j; + int index; + + da = (float) Math.PI / (float) arcSlices; + dy = length / (float) lengthSlices; + if (stacks != 0.0f) + dz = height / (float) stacks; + else + dz = 0.0f; + z = 0.0f; + index = 0; + + for (j = 0; j <= stacks; j++) + { + // Top arc + for (i = 0; i <= arcSlices; i++) + { + a = i * da + (3.0f * (float) Math.PI / 2.0f); + x = (float) Math.sin(a); + y = (float) Math.cos(a); + dest[index++] = x * radius; + dest[index++] = y * radius + length; + dest[index++] = z; + } + // Right side. + for (i = lengthSlices - 1; i >= 1; i--) + { + dest[index++] = radius; + dest[index++] = i * dy; + dest[index++] = z; + } + // Bottom arc + for (i = 0; i <= arcSlices; i++) + { + a = i * da + ((float) Math.PI / 2.0f); + x = (float) Math.sin(a); + y = (float) Math.cos(a); + dest[index++] = x * radius; + dest[index++] = y * radius; + dest[index++] = z; + } + // Left side. + for (i = 1; i < lengthSlices; i++) + { + dest[index++] = -radius; + dest[index++] = i * dy; + dest[index++] = z; + } + z += dz; + } + } + + public void makeLongCylinderNormals(int arcSlices, int lengthSlices, int stacks, float[] dest) + { + int numPoints = this.getLongCylinderVertexCount(arcSlices, lengthSlices, stacks); + int numCoords = 3 * numPoints; + + if (numPoints < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "arcSlices=" + arcSlices + + " lengthSlices=" + lengthSlices + " stacks=" + stacks); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + float x, y; + float a, da; + float nsign; + int i, j; + int index; + + da = (float) Math.PI / (float) arcSlices; + nsign = (this.orientation == OUTSIDE) ? 1.0f : -1.0f; + index = 0; + + for (j = 0; j <= stacks; j++) + { + // Top arc + for (i = 0; i <= arcSlices; i++) + { + a = i * da + (3.0f * (float) Math.PI / 2.0f); + x = (float) Math.sin(a); + y = (float) Math.cos(a); + dest[index++] = x * nsign; + dest[index++] = y * nsign; + dest[index++] = 0.0f; + } + // Right side. + for (i = lengthSlices - 1; i >= 1; i--) + { + //noinspection PointlessArithmeticExpression + dest[index++] = 1.0f * nsign; + dest[index++] = 0.0f; + dest[index++] = 0.0f; + } + // Bottom arc + for (i = 0; i <= arcSlices; i++) + { + a = i * da + ((float) Math.PI / 2.0f); + x = (float) Math.sin(a); + y = (float) Math.cos(a); + dest[index++] = x * nsign; + dest[index++] = y * nsign; + dest[index++] = 0.0f; + } + // Left side. + for (i = 1; i < lengthSlices; i++) + { + dest[index++] = -1.0f * nsign; + dest[index++] = 0.0f; + dest[index++] = 0.0f; + } + } + } + + public void makeLongCylinderIndices(int arcSlices, int lengthSlices, int stacks, int[] dest) + { + int numIndices = this.getLongCylinderIndexCount(arcSlices, lengthSlices, stacks); + + if (numIndices < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "arcSlices=" + arcSlices + + " lengthSlices=" + lengthSlices + " stacks=" + stacks); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numIndices) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int slices; + int i, j; + int vertex, index; + + slices = 2 * (arcSlices + 1) + 2 * (lengthSlices - 1); + index = 0; + + for (j = 0; j < stacks; j++) + { + if (j != 0) + { + if (this.orientation == INSIDE) + { + vertex = (j - 1) * slices; + dest[index++] = vertex; + vertex = j * slices; + dest[index++] = vertex; + } + else // (this.orientation == OUTSIDE) + { + vertex = (j - 1) * slices; + dest[index++] = vertex + slices; + vertex = (j - 1) * slices; + dest[index++] = vertex; + } + } + for (i = 0; i <= slices; i++) + { + if (i == slices) + vertex = j * slices; + else + vertex = i + j * slices; + if (this.orientation == INSIDE) + { + dest[index++] = vertex + slices; + dest[index++] = vertex; + } + else // (this.orientation == OUTSIDE) + { + dest[index++] = vertex; + dest[index++] = vertex + slices; + } + } + } + } + + public void makeLongCylinderOutlineIndices(int arcSlices, int lengthSlices, int stacks, int[] dest) + { + int numIndices = this.getLongCylinderOutlineIndexCount(arcSlices, lengthSlices, stacks); + + if (numIndices < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "arcSlices=" + arcSlices + + " lengthSlices=" + lengthSlices + " stacks=" + stacks); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numIndices) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int slices = 2 * (arcSlices + 1) + 2 * (lengthSlices - 1); + int i; + int vertex, index; + + index = 0; + // Bottom ring + for (i = 0; i < slices; i++) + { + vertex = i; + dest[index++] = vertex; + dest[index++] = (i != slices - 1) ? vertex + 1 : 0; + } + // Top ring + for (i = 0; i < slices; i++) + { + vertex = i + slices * stacks; + dest[index++] = vertex; + dest[index++] = (i != slices - 1) ? vertex + 1 : slices * stacks; + } + } + + //**************************************************************// + //******************** Long Disk ********************// + //**************************************************************// + + public int getLongDiskVertexCount(int arcSlices, int lengthSlices, int loops) + { + int slices = 2 * (arcSlices + 1) + 2 * (lengthSlices - 1); + return slices * (loops + 1); + } + + public int getLongDiskIndexCount(int arcSlices, int lengthSlices, int loops) + { + int slices = 2 * (arcSlices + 1) + 2 * (lengthSlices - 1); + return loops * 2 * (slices + 1) + 2 * (loops - 1); + } + + public int getLongDiskDrawMode() + { + return GL_TRIANGLE_STRIP; + } + + public void makeLongDiskVertices(float innerRadius, float outerRadius, float length, + int arcSlices, int lengthSlices, int loops, float[] dest) + { + int numPoints = this.getLongDiskVertexCount(arcSlices, lengthSlices, loops); + int numCoords = 3 * numPoints; + + if (numPoints < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "arcSlices=" + arcSlices + + " lengthSlices=" + lengthSlices + " loops=" + loops); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + float x, y; + float a, r; + float dy, da, dr; + int s, l; + int index; + + dy = length / (float) lengthSlices; + da = (float) Math.PI / (float) arcSlices; + dr = (outerRadius - innerRadius) / (float) loops; + index = 0; + + for (l = 0; l <= loops; l++) + { + r = innerRadius + l * dr; + // Top arc. + for (s = 0; s <= arcSlices; s++) + { + a = s * da + (3.0f * (float) Math.PI / 2.0f); + x = (float) Math.sin(a); + y = (float) Math.cos(a); + dest[index++] = x * r; + dest[index++] = y * r + length; + dest[index++] = 0.0f; + } + // Right side. + for (s = lengthSlices - 1; s >= 1; s--) + { + dest[index++] = r; + dest[index++] = s * dy; + dest[index++] = 0.0f; + } + // Bottom arc. + for (s = 0; s <= arcSlices; s++) + { + a = s * da + ((float) Math.PI / 2.0f); + x = (float) Math.sin(a); + y = (float) Math.cos(a); + dest[index++] = x * r; + dest[index++] = y * r; + dest[index++] = 0.0f; + } + // Left side. + for (s = 1; s < lengthSlices; s++) + { + dest[index++] = -r; + dest[index++] = s * dy; + dest[index++] = 0.0f; + } + } + } + + public void makeLongDiskNormals(int arcSlices, int lengthSlices, int loops, float[] dest) + { + int numPoints = this.getLongDiskVertexCount(arcSlices, lengthSlices, loops); + int numCoords = 3 * numPoints; + + if (numPoints < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "arcSlices=" + arcSlices + + " lengthSlices=" + lengthSlices + " loops=" + loops); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int slices; + int s, l; + int index; + float nsign; + float[] normal; + + slices = 2 * (arcSlices + 1) + 2 * (lengthSlices - 1); + + nsign = (this.orientation == OUTSIDE) ? 1.0f : -1.0f; + normal = new float[3]; + normal[0] = 0.0f; + normal[1] = 0.0f; + //noinspection PointlessArithmeticExpression + normal[2] = 1.0f * nsign; + + for (l = 0; l <= loops; l++) + { + for (s = 0; s < slices; s++) + { + index = s + l * slices; + index = 3 * index; + System.arraycopy(normal, 0, dest, index, 3); + } + } + } + + @SuppressWarnings({"UnusedDeclaration"}) + public void makeLongDiskVertexNormals(float innerRadius, float outerRadius, float length, + int arcSlices, int lengthSlices, int loops, + float[] srcVerts, float[] dest) + { + int numPoints = this.getLongDiskVertexCount(arcSlices, lengthSlices, loops); + int numCoords = 3 * numPoints; + + if (numPoints < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "arcSlices=" + arcSlices + + " lengthSlices=" + lengthSlices + " loops=" + loops); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (srcVerts == null) + { + String message = "nullValue.SourceVertexArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int slices; + int s, l; + int index; + float nsign; + float[] norm, zero, tmp; + + slices = 2 * (arcSlices + 1) + 2 * (lengthSlices - 1); + nsign = (this.orientation == OUTSIDE) ? 1.0f : -1.0f; + norm = new float[3]; + zero = new float[3]; + tmp = new float[3]; + + for (l = 0; l <= loops; l++) + { + // Normal vectors for first and last loops require a special case. + if (l == 0 || l == loops) + { + // Closed disk: slices are collapsed. + if (l == 0 && innerRadius == 0.0f) + { + // Top arc. + { + // Compute common normal. + System.arraycopy(zero, 0, norm, 0, 3); + for (s = 0; s <= arcSlices; s++) + { + index = s; + this.facenorm(srcVerts, index, index + slices + 1, index + slices, tmp); + this.add3AndSet(norm, 0, tmp, 0); + } + index = arcSlices; + this.facenorm(srcVerts, index, index + 1, index + slices, tmp); + this.add3AndSet(norm, 0, tmp, 0); + index = 0; + this.facenorm(srcVerts, index, index + slices, index + slices - 1, tmp); + this.add3AndSet(norm, 0, tmp, 0); + this.mul3AndSet(norm, 0, nsign); + this.norm3AndSet(norm, 0); + // Copy common normal to the first point of each slice. + for (s = 0; s <= arcSlices; s++) + { + index = s; + System.arraycopy(norm, 0, dest, 3 * index, 3); + } + } + // Right and left sides. + { + int leftSideIndex; + for (s = 1; s < lengthSlices; s++) + { + // Compute common normal. + index = s + arcSlices; + leftSideIndex = slices - s; + System.arraycopy(zero, 0, norm, 0, 3); + this.facenorm(srcVerts, index, index + slices, index - 1, tmp); + this.add3AndSet(norm, 0, tmp, 0); + this.facenorm(srcVerts, index, index + 1, index + slices, tmp); + this.add3AndSet(norm, 0, tmp, 0); + if (s == 1) + this.facenorm(srcVerts, leftSideIndex, leftSideIndex - slices + 1, + leftSideIndex + slices, tmp); + else + this.facenorm(srcVerts, leftSideIndex, leftSideIndex + 1, leftSideIndex + slices, tmp); + this.add3AndSet(norm, 0, tmp, 0); + this.facenorm(srcVerts, leftSideIndex, leftSideIndex + slices, leftSideIndex - 1, tmp); + this.add3AndSet(norm, 0, tmp, 0); + this.mul3AndSet(norm, 0, nsign); + this.norm3AndSet(norm, 0); + // Copy common normal to the first point of each slice. + System.arraycopy(norm, 0, dest, 3 * index, 3); + System.arraycopy(norm, 0, dest, 3 * leftSideIndex, 3); + } + } + // Bottom arc. + { + // Compute common normal. + System.arraycopy(zero, 0, norm, 0, 3); + for (s = 0; s <= arcSlices; s++) + { + index = s + arcSlices + lengthSlices; + this.facenorm(srcVerts, index, index + slices + 1, index + slices, tmp); + this.add3AndSet(norm, 0, tmp, 0); + } + index = arcSlices + lengthSlices; + this.facenorm(srcVerts, index, index + slices, index - 1, tmp); + this.add3AndSet(norm, 0, tmp, 0); + index = (2 * arcSlices) + lengthSlices; + this.facenorm(srcVerts, index, index + 1, index + slices, tmp); + this.add3AndSet(norm, 0, tmp, 0); + this.mul3AndSet(norm, 0, nsign); + this.norm3AndSet(norm, 0); + // Copy common normal to the first point of each slice. + for (s = 0; s <= arcSlices; s++) + { + index = s + arcSlices + lengthSlices; + System.arraycopy(norm, 0, dest, 3 * index, 3); + } + } + } + // Open disk: each slice has a unique starting point. + else + { + for (s = 0; s < slices; s++) + { + int prevSlice, nextSlice; + int adjacentLoop; + index = s + l * slices; + prevSlice = index - 1; + nextSlice = index + 1; + + if (s == 0) + prevSlice = l * slices; + else if (s == slices - 1) + nextSlice = l; + + if (l == 0) + adjacentLoop = index + slices; + else + adjacentLoop = index - slices; + + System.arraycopy(zero, 0, norm, 0, 3); + + // Add clockwise adjacent face. + if (l == 0) + this.facenorm(srcVerts, index, nextSlice, adjacentLoop, tmp); + else + this.facenorm(srcVerts, index, adjacentLoop, nextSlice, tmp); + this.add3AndSet(norm, 0, tmp, 0); + // Add counter-clockwise adjacent face. + if (l == 0) + this.facenorm(srcVerts, index, adjacentLoop, prevSlice, tmp); + else + this.facenorm(srcVerts, index, prevSlice, adjacentLoop, tmp); + this.add3AndSet(norm, 0, tmp, 0); + + // Normalize and place in output. + this.mul3AndSet(norm, 0, nsign); + this.norm3AndSet(norm, 0); + System.arraycopy(norm, 0, dest, 3 * index, 3); + } + } + } + // Normal vectors for internal loops. + else + { + for (s = 0; s < slices; s++) + { + int prevSlice, nextSlice; + int prevLoop, nextLoop; + index = s + l * slices; + prevSlice = index - 1; + nextSlice = index + 1; + + if (s == 0) + prevSlice = (slices - 1) + l * slices; + else if (s == slices - 1) + nextSlice = l * slices; + + prevLoop = index - slices; + nextLoop = index + slices; + + System.arraycopy(zero, 0, norm, 0, 3); + + // Add lower-left adjacent face. + this.facenorm(srcVerts, index, prevSlice, prevSlice - slices, tmp); + this.add3AndSet(norm, 0, tmp, 0); + this.facenorm(srcVerts, index, prevSlice - slices, prevLoop, tmp); + this.add3AndSet(norm, 0, tmp, 0); + // Add lower-right adjacent face. + this.facenorm(srcVerts, index, prevLoop, nextSlice - slices, tmp); + this.add3AndSet(norm, 0, tmp, 0); + this.facenorm(srcVerts, index, nextSlice - slices, nextSlice, tmp); + this.add3AndSet(norm, 0, tmp, 0); + // Add upper-right adjacent face. + this.facenorm(srcVerts, index, nextSlice, nextSlice + slices, tmp); + this.add3AndSet(norm, 0, tmp, 0); + this.facenorm(srcVerts, index, nextSlice + slices, nextLoop, tmp); + this.add3AndSet(norm, 0, tmp, 0); + // Add upper-left adjacent face. + this.facenorm(srcVerts, index, nextLoop, prevSlice + slices, tmp); + this.add3AndSet(norm, 0, tmp, 0); + this.facenorm(srcVerts, index, prevSlice + slices, prevSlice, tmp); + this.add3AndSet(norm, 0, tmp, 0); + + // Normalize and place in output. + this.mul3AndSet(norm, 0, nsign); + this.norm3AndSet(norm, 0); + System.arraycopy(norm, 0, dest, 3 * index, 3); + } + } + } + } + + public void makeLongDiskIndices(int arcSlices, int lengthSlices, int loops, int[] dest) + { + int numIndices = this.getLongDiskIndexCount(arcSlices, lengthSlices, loops); + + if (numIndices < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "arcSlices=" + arcSlices + + " lengthSlices=" + lengthSlices + " loops=" + loops); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numIndices) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int slices; + int s, l; + int vertex, index; + + slices = 2 * (arcSlices + 1) + 2 * (lengthSlices - 1); + index = 0; + + for (l = 0; l < loops; l++) + { + if (l != 0) + { + if (this.orientation == INSIDE) + { + vertex = (l - 1) * slices; + dest[index++] = vertex + slices; + vertex = (l - 1) * slices; + dest[index++] = vertex; + } + else // (this.orientation == OUTSIDE) + { + vertex = (l - 1) * slices; + dest[index++] = vertex; + vertex = l * slices; + dest[index++] = vertex; + } + } + for (s = 0; s <= slices; s++) + { + if (s == slices) + vertex = l * slices; + else + vertex = s + l * slices; + if (this.orientation == INSIDE) + { + dest[index++] = vertex; + dest[index++] = vertex + slices; + } + else // (this.orientation == OUTSIDE) + { + dest[index++] = vertex + slices; + dest[index++] = vertex; + } + } + } + } + + //**************************************************************// + //******************** Polygon ****************// + //**************************************************************// + + public int computePolygonWindingOrder2(int pos, int count, Vec4[] points) + { + float area; + int order; + + area = this.computePolygonArea2(pos, count, points); + if (area < 0.0f) + order = CLOCKWISE; + else + order = COUNTER_CLOCKWISE; + + return order; + } + + public float computePolygonArea2(int pos, int count, Vec4[] points) + { + if (pos < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "pos=" + pos); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (count < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "count=" + count); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (points == null) + { + String message = "nullValue.PointsIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (points.length < (pos + count)) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "points.length < " + (pos + count)); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + float area; + int i; + int coord, nextCoord; + + area = 0.0f; + for (i = 0; i < count; i++) + { + coord = pos + i; + nextCoord = (i == count - 1) ? (pos) : (pos + i + 1); + area += points[coord].x * points[nextCoord].y; + area -= points[nextCoord].x * points[coord].y; + } + area /= 2.0f; + + return area; + } + +// public IndexedTriangleArray tessellatePolygon(int pos, int count, float[] vertices, Vec4 normal) +// { +// if (count < 0) +// { +// String message = Logging.getMessage("generic.ArgumentOutOfRange", "count=" + count); +// Logging.error(message); +// throw new IllegalArgumentException(message); +// } +// if (vertices == null) +// { +// String message = "nullValue.VertexArrayIsNull"; +// Logging.error(message); +// throw new IllegalArgumentException(message); +// } +// if (vertices.length < (pos + count)) +// { +// String message = Logging.getMessage("generic.ArrayInvalidLength", "vertices.length=" + vertices.length); +// Logging.error(message); +// throw new IllegalArgumentException(message); +// } +// +// TessellatorCallback cb; +// GLUTessellatorSupport glts; +// double[] dvertices = new double[3 * count]; +// int i; +// int srcIndex, destIndex; +// +// if (normal == null) +// normal = Vec4.UNIT_Z; +// +// cb = new TessellatorCallback(this, count, vertices); +// glts = new GLUTessellatorSupport(); +// glts.beginTessellation(cb, normal); +// try +// { +// GLU.gluTessBeginPolygon(glts.getGLUtessellator(), null); +// GLU.gluTessBeginContour(glts.getGLUtessellator()); +// for (i = 0; i < count; i++) +// { +// srcIndex = 3 * (pos + i); +// destIndex = 3 * i; +// dvertices[destIndex] = vertices[srcIndex]; +// dvertices[destIndex + 1] = vertices[srcIndex + 1]; +// dvertices[destIndex + 2] = vertices[srcIndex + 2]; +// GLU.gluTessVertex(glts.getGLUtessellator(), dvertices, destIndex, pos + i); +// } +// GLU.gluTessEndContour(glts.getGLUtessellator()); +// GLU.gluTessEndPolygon(glts.getGLUtessellator()); +// } +// finally +// { +// glts.endTessellation(); +// } +// +// return new IndexedTriangleArray( +// cb.getIndexCount(), cb.getIndices(), +// cb.getVertexCount(), cb.getVertices()); +// } + +// public IndexedTriangleArray tessellatePolygon2(int pos, int count, float[] vertices) +// { +// if (count < 0) +// { +// String message = Logging.getMessage("generic.ArgumentOutOfRange", "count=" + count); +// Logging.error(message); +// throw new IllegalArgumentException(message); +// } +// if (vertices == null) +// { +// String message = "nullValue.VertexArrayIsNull"; +// Logging.error(message); +// throw new IllegalArgumentException(message); +// } +// if (vertices.length < (pos + count)) +// { +// String message = Logging.getMessage("generic.ArrayInvalidLength", "vertices.length=" + vertices.length); +// Logging.error(message); +// throw new IllegalArgumentException(message); +// } +// +// return this.tessellatePolygon(pos, count, vertices, Vec4.UNIT_Z); +// } + +// private static class TessellatorCallback extends GLUtessellatorCallbackAdapter +// { +// private GeometryBuilder gb; +// private int type; +// private int indexCount; +// private int primIndexCount; +// private int vertexCount; +// private int[] indices; +// private int[] primIndices; +// private float[] vertices; +// +// private TessellatorCallback(GeometryBuilder gb, int vertexCount, float[] vertices) +// { +// this.gb = gb; +// this.indexCount = 0; +// this.primIndexCount = 0; +// this.vertexCount = vertexCount; +// +// int initialCapacity = this.gb.nextPowerOfTwo(3 * vertexCount); +// this.indices = new int[initialCapacity]; +// this.primIndices = new int[initialCapacity]; +// this.vertices = this.gb.copyOf(vertices, initialCapacity); +// } +// +// public int getIndexCount() +// { +// return this.indexCount; +// } +// +// public int[] getIndices() +// { +// return this.indices; +// } +// +// public int getVertexCount() +// { +// return this.vertexCount; +// } +// +// public float[] getVertices() +// { +// return this.vertices; +// } +// +// protected void addTriangle(int i1, int i2, int i3) +// { +// // Triangle indices will be specified in counter-clockwise order. To reverse the ordering, we +// // swap the indices. +// +// int minCapacity, oldCapacity, newCapacity; +// +// minCapacity = this.indexCount + 3; +// oldCapacity = this.indices.length; +// while (minCapacity > oldCapacity) +// { +// newCapacity = 2 * oldCapacity; +// this.indices = this.gb.copyOf(this.indices, newCapacity); +// oldCapacity = minCapacity; +// } +// +// if (this.gb.orientation == GeometryBuilder.INSIDE) +// { +// this.indices[this.indexCount++] = this.primIndices[i1]; +// this.indices[this.indexCount++] = this.primIndices[i3]; +// this.indices[this.indexCount++] = this.primIndices[i2]; +// } +// else // (this.gb.orientation == GeometryBuilder.OUTSIDE) +// { +// this.indices[this.indexCount++] = this.primIndices[i1]; +// this.indices[this.indexCount++] = this.primIndices[i2]; +// this.indices[this.indexCount++] = this.primIndices[i3]; +// } +// } +// +// public void begin(int type) +// { +// this.type = type; +// this.primIndexCount = 0; +// } +// +// public void vertex(Object vertexData) +// { +// int minCapacity, oldCapacity, newCapacity; +// +// oldCapacity = this.primIndices.length; +// minCapacity = this.primIndexCount + 1; +// while (minCapacity > oldCapacity) +// { +// newCapacity = 2 * oldCapacity; +// this.primIndices = this.gb.copyOf(this.primIndices, newCapacity); +// oldCapacity = newCapacity; +// } +// +// int index = (Integer) vertexData; +// this.primIndices[this.primIndexCount++] = index; +// } +// +// public void end() +// { +// int i; +// +// if (this.type == GL_TRIANGLES) +// { +// for (i = 2; i < this.primIndexCount; i++) +// { +// if (((i + 1) % 3) == 0) +// this.addTriangle(i - 2, i - 1, i); +// } +// } +// else if (this.type == GL_TRIANGLE_STRIP) +// { +// for (i = 2; i < this.primIndexCount; i++) +// { +// if ((i % 2) == 0) +// this.addTriangle(i - 2, i - 1, i); +// else +// this.addTriangle(i - 1, i - 2, i); +// } +// } +// else if (this.type == GL_TRIANGLE_FAN) +// { +// for (i = 2; i < this.primIndexCount; i++) +// { +// this.addTriangle(0, i - 1, i); +// } +// } +// } +// +// public void combine(double[] coords, Object[] data, float[] weight, Object[] outData) +// { +// outData[0] = data[0]; +// } +// } + + //**************************************************************// + //******************** Indexed Triangle Buffer ***************// + //**************************************************************// + + public int getIndexedTriangleBufferDrawMode() + { + return GL_TRIANGLES; + } + + public static class IndexedTriangleBuffer + { + private IntBuffer indices; + private FloatBuffer vertices; + private int indexCount; + private int vertexCount; + + public IndexedTriangleBuffer(int indexCount, IntBuffer indices, int vertexCount, FloatBuffer vertices) + { + this.indices = indices; + this.vertices = vertices; + this.indexCount = indexCount; + this.vertexCount = vertexCount; + } + + public int getIndexCount() + { + return this.indexCount; + } + + public IntBuffer getIndices() + { + return this.indices; + } + + public int getVertexCount() + { + return this.vertexCount; + } + + public FloatBuffer getVertices() + { + return this.vertices; + } + } + + public void subdivideIndexedTriangleBuffer(IndexedTriangleBuffer itb) + { + if (itb == null) + { + String message = "nullValue.IndexedTriangleArray"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int indexCount; + int a, b, c; + int ab, bc, ca; + int i, j; + HashMap edgeMap; + Edge e; + Integer split; + + indexCount = itb.getIndexCount(); + edgeMap = new HashMap(); + + // Iterate over each triangle, and split the edge of each triangle. Each edge is split exactly once. The + // index of the new vertex created by a split is stored in edgeMap. + for (i = 0; i < indexCount; i += 3) + { + for (j = 0; j < 3; j++) + { + a = itb.indices.get(i + j); + b = itb.indices.get((j < 2) ? (i + j + 1) : i); + e = new Edge(a, b); + split = edgeMap.get(e); + if (split == null) + { + split = this.splitVertex(itb, a, b); + edgeMap.put(e, split); + } + } + } + + // Iterate over each triangle, and create indices for four new triangles, replacing indices of the original + // triangle. + for (i = 0; i < indexCount; i += 3) + { + a = itb.indices.get(i); + b = itb.indices.get(i + 1); + c = itb.indices.get(i + 2); + ab = edgeMap.get(new Edge(a, b)); + bc = edgeMap.get(new Edge(b, c)); + ca = edgeMap.get(new Edge(c, a)); + this.indexSplitTriangle(itb, i, a, b, c, ab, bc, ca); + } + } + + public void makeIndexedTriangleBufferNormals(IndexedTriangleBuffer itb, FloatBuffer dest) + { + if (itb == null) + { + String message = "nullValue.IndexedTriangleArray"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int numCoords = 3 * itb.vertexCount; + + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.capacity() < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + dest.capacity(); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.makeIndexedTriangleBufferNormals(0, itb.getIndexCount(), itb.getIndices(), + 0, itb.getVertexCount(), itb.getVertices(), dest); + } + + public void makeIndexedTriangleBufferNormals(int indexPos, int indexCount, IntBuffer indices, + int vertexPos, int vertexCount, FloatBuffer vertices, + FloatBuffer dest) + { + if (indices == null) + { + String message = "nullValue.IndexArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (indices.capacity() < (indexPos + indexCount)) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "indices.length=" + indices.capacity()); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (vertices == null) + { + String message = "nullValue.VertexArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (vertices.capacity() < (vertexPos + vertexCount)) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "vertices.length=" + vertices.capacity()); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.capacity() < (vertexPos + vertexCount)) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "dest.length=" + dest.capacity()); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int i, v; + int index; + float nsign; + float[] norm; + int[] faceIndices; + + nsign = (this.orientation == OUTSIDE) ? 1.0f : -1.0f; + norm = new float[3]; + faceIndices = new int[3]; + + // Compute the normal for each face, contributing that normal to each vertex of the face. + for (i = 0; i < indexCount; i += 3) + { + faceIndices[0] = indices.get(indexPos + i); + faceIndices[1] = indices.get(indexPos + i + 1); + faceIndices[2] = indices.get(indexPos + i + 2); + // Compute the normal for this face. + this.facenorm(vertices, faceIndices[0], faceIndices[1], faceIndices[2], norm); + // Add this face normal to the normal at each vertex. + for (v = 0; v < 3; v++) + { + index = 3 * faceIndices[v]; + this.add3AndSet(dest, index, norm, 0); + } + } + + // Scale and normalize each vertex normal. + for (v = 0; v < vertexCount; v++) + { + index = 3 * (vertexPos + v); + this.mul3AndSet(dest, index, nsign); + this.norm3AndSet(dest, index); + } + + dest.rewind(); + } + + private int splitVertex(IndexedTriangleBuffer itb, int a, int b) + { + int minCapacity, oldCapacity, newCapacity; + + oldCapacity = itb.vertices.capacity(); + minCapacity = 3 * (itb.getVertexCount() + 1); + while (minCapacity > oldCapacity) + { + newCapacity = 2 * oldCapacity; + itb.vertices = this.copyOf(itb.vertices, newCapacity); + oldCapacity = newCapacity; + } + + int s = itb.getVertexCount(); + int is = 3 * s; + int ia = 3 * a; + int ib = 3 * b; + itb.vertices.put(is, (itb.vertices.get(ia) + itb.vertices.get(ib)) / 2.0f); + itb.vertices.put(is + 1, (itb.vertices.get(ia + 1) + itb.vertices.get(ib + 1)) / 2.0f); + itb.vertices.put(is + 2, (itb.vertices.get(ia + 2) + itb.vertices.get(ib + 2)) / 2.0f); + itb.vertexCount++; + + return s; + } + + public void makeEllipsoidNormals(IndexedTriangleBuffer itb, FloatBuffer dest) + { + if (itb == null) + { + String message = "nullValue.IndexedTriangleArray"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int numCoords = 3 * itb.vertexCount; + + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.capacity() < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + dest.capacity(); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.makeEllipsoidNormals(0, itb.getIndexCount(), itb.getIndices(), + 0, itb.getVertexCount(), itb.getVertices(), dest); + } + + public void makeEllipsoidNormals(int indexPos, int indexCount, IntBuffer indices, + int vertexPos, int vertexCount, FloatBuffer vertices, + FloatBuffer dest) + { + if (indices == null) + { + String message = "nullValue.IndexArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (indices.capacity() < (indexPos + indexCount)) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "indices.length=" + indices.capacity()); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (vertices == null) + { + String message = "nullValue.VertexArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (vertices.capacity() < (vertexPos + vertexCount)) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "vertices.length=" + vertices.capacity()); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.capacity() < (vertexPos + vertexCount)) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "dest.length=" + dest.capacity()); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int i, v; + int index; + float nsign; + + nsign = (this.orientation == OUTSIDE) ? 1.0f : -1.0f; + + // for a sphere, normals are just the normalized vectors of the vertex positions + + // first copy all the vertices to the normals buffer + for (i = 0; i < 3 * vertexCount; i++) + { + dest.put(i, vertices.get(i)); + } + + // Scale and normalize each vertex normal. + for (v = 0; v < vertexCount; v++) + { + index = 3 * (vertexPos + v); + this.mul3AndSet(dest, index, nsign); + this.norm3AndSet(dest, index); + } + + dest.rewind(); + } + + public void makeCylinderNormals(IndexedTriangleBuffer itb, FloatBuffer dest) + { + if (itb == null) + { + String message = "nullValue.IndexedTriangleArray"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int numCoords = 3 * itb.vertexCount; + + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.capacity() < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + dest.capacity(); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.makeCylinderNormals(0, itb.getIndexCount(), itb.getIndices(), + 0, itb.getVertexCount(), itb.getVertices(), dest); + } + + public void makeCylinderNormals(int indexPos, int indexCount, IntBuffer indices, + int vertexPos, int vertexCount, FloatBuffer vertices, + FloatBuffer dest) + { + if (indices == null) + { + String message = "nullValue.IndexArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (indices.capacity() < (indexPos + indexCount)) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "indices.length=" + indices.capacity()); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (vertices == null) + { + String message = "nullValue.VertexArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (vertices.capacity() < (vertexPos + vertexCount)) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "vertices.length=" + vertices.capacity()); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.capacity() < (vertexPos + vertexCount)) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "dest.length=" + dest.capacity()); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int i, v; + int index; + float nsign; + + nsign = (this.orientation == OUTSIDE) ? 1.0f : -1.0f; + + // for a cylinder, normals are just the normalized vectors of the (x, y) coords of the vertex positions + + // first copy all the vertices to the normals buffer + for (i = 0; i < 3 * vertexCount; i++) + { + if (i % 3 == 2) // set z coord to zero + dest.put(i, 0); + else + dest.put(i, -vertices.get(i)); + } + + // Scale and normalize each vertex normal. + for (v = 0; v < vertexCount; v++) + { + index = 3 * (vertexPos + v); + this.mul3AndSet(dest, index, nsign); + this.norm3AndSet(dest, index); + } + + dest.rewind(); + } + + private void indexSplitTriangle(IndexedTriangleBuffer itb, int original, int a, int b, int c, int ab, int bc, + int ca) + { + int minCapacity, oldCapacity, newCapacity; + + // One of the new triangles will overwrite the original triangles, so we only need enough space to index + // three new triangles. + oldCapacity = itb.indices.capacity(); + minCapacity = itb.getIndexCount() + 9; + while (minCapacity > oldCapacity) + { + newCapacity = 2 * oldCapacity; + itb.indices = this.copyOf(itb.indices, newCapacity); + oldCapacity = newCapacity; + } + + // Lower-left triangle. + // This triangle replaces the original. + itb.indices.put(original, a); + itb.indices.put(original + 1, ab); + itb.indices.put(original + 2, ca); + + // Center triangle. + itb.indices.put(itb.indexCount++, ab); + itb.indices.put(itb.indexCount++, bc); + itb.indices.put(itb.indexCount++, ca); + + // Lower-right triangle. + itb.indices.put(itb.indexCount++, ab); + itb.indices.put(itb.indexCount++, b); + itb.indices.put(itb.indexCount++, bc); + + // Upper triangle. + itb.indices.put(itb.indexCount++, ca); + itb.indices.put(itb.indexCount++, bc); + itb.indices.put(itb.indexCount++, c); + } + + // This method finds triangles of the sphere mesh which span the discontinuity at 2 * PI radians. + // It then creates a duplicate of the vertex in each of these triangles that is out of range, and uses this + // duplicate to form the face (replacing the original vertex). This way, the original vertex is used only + // in faces that do not span the discontinuity, while faces that do span the discontinuity use the + // duplicate instead. When it comes time for texture mapping, a different texture coordinate can be + // mapped to the duplicate vertex than to the original, each one falling in the correct range for the face(s) it + // comprises. + + public void fixSphereSeam(IndexedTriangleBuffer itb, float wrapThreshold) + { + int vertex0, vertex1, vertex2; // indices of the three vertices of the current face + double x0, y0, x1, y1, x2, y2; // actual x and y point values of those vertices + double phi0, phi1, phi2; + + Integer newVertex, wrapVertex; + int wrapIndex = -1; // track index of the vertex that will be replaced by a new "wrapped" vertex + // keep track of newly created duplicate vertices: + Map duplicates = new HashMap(); + + // for each indexed triangle, determine if phi (longitude) of any of the vertices is on the + // opposite side of 2PI from others (the "wrap" vertex) + int indexCount = itb.getIndexCount(); + for (int i = 0; i < indexCount; i += 3) + { + vertex0 = itb.indices.get(i); + vertex1 = itb.indices.get(i + 1); + vertex2 = itb.indices.get(i + 2); + + x0 = itb.vertices.get(3 * vertex0); + y0 = itb.vertices.get(3 * vertex0 + 1); + x1 = itb.vertices.get(3 * vertex1); + y1 = itb.vertices.get(3 * vertex1 + 1); + x2 = itb.vertices.get(3 * vertex2); + y2 = itb.vertices.get(3 * vertex2 + 1); + + // compute phi of each of the three vertices of the face + phi0 = Math.atan2(y0, x0); + if (phi0 < 0.0d) + phi0 += 2.0d * Math.PI; + + phi1 = Math.atan2(y1, x1); + if (phi1 < 0.0d) + phi1 += 2.0d * Math.PI; + + phi2 = Math.atan2(y2, x2); + if (phi2 < 0.0d) + phi2 += 2.0d * Math.PI; + + // check if face spans phi = 0 (the texture seam), and determine which is the "wrapped" vertex + if (Math.abs(phi0 - phi1) > wrapThreshold) + { + if (Math.abs(phi0 - phi2) > wrapThreshold) + wrapIndex = i; // vertex0 is the wrapped vertex + else + wrapIndex = i + 1; // vertex1 is the wrapped vertex + } + else if (Math.abs(phi1 - phi2) > wrapThreshold) + wrapIndex = i + 2; // vertex2 is the wrapped vertex + + if (wrapIndex >= 0) // check if one of the vertices on this face wrapped across 2PI + { + wrapVertex = itb.indices.get(wrapIndex); + //look to see if this vertex has been duplicated already + newVertex = duplicates.get(wrapVertex); + if (newVertex != null) + itb.indices.put(wrapIndex, newVertex); // replace the old vertex with the duplicate + else + { + // create a duplicate of the wrapIndex vertex and get its index newVertex + newVertex = duplicateVertex(itb, wrapVertex); + // place the new vertex in the duplicates structure + duplicates.put(wrapVertex, newVertex); + // now replace the index at the wrapIndex with the index of the new duplicate + itb.indices.put(wrapIndex, newVertex); + } + wrapIndex = -1; // reset the wrapVertex + } + } + } + + // append copy of vertex at sourceIndex to end of vertices buffer + + private int duplicateVertex(IndexedTriangleBuffer itb, int sourceIndex) + { + if (itb == null) + { + String message = Logging.getMessage("nullValue.IndexedTriangleBufferIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (sourceIndex >= itb.vertexCount) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "sourceIndex > vertexCount"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // ensure there is room in the vertex buffer for one new vertex + int minCapacity, oldCapacity, newCapacity; + + oldCapacity = itb.vertices.capacity(); + minCapacity = 3 * itb.getVertexCount() + 3; + while (minCapacity > oldCapacity) + { + newCapacity = 2 * oldCapacity; + itb.vertices = this.copyOf(itb.vertices, newCapacity); + oldCapacity = newCapacity; + } + + int destIndex = itb.getVertexCount(); + + // append vertex data to vertices buffer + itb.vertices.put(3 * destIndex, itb.vertices.get(3 * sourceIndex)); + itb.vertices.put(3 * destIndex + 1, itb.vertices.get(3 * sourceIndex + 1)); + itb.vertices.put(3 * destIndex + 2, itb.vertices.get(3 * sourceIndex + 2)); + + itb.vertexCount++; + // return index of the newly created vertex + return itb.vertexCount - 1; + } + + public void makeUnitSphereTextureCoordinates(IndexedTriangleBuffer itb, FloatBuffer texCoords) + { + if (itb == null) + { + String message = "nullValue.IndexedTriangleArray"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int numCoords = 2 * itb.vertexCount; + + if (texCoords == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (texCoords.capacity() < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + texCoords.capacity(); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.makeUnitSphereTextureCoordinates(itb.getVertexCount(), itb.getVertices(), texCoords, -1); + } + + // allow for correction of seam caused by triangles that wrap across tecture bounds + + public void makeUnitSphereTextureCoordinates(IndexedTriangleBuffer itb, FloatBuffer texCoords, + int seamVerticesIndex) + { + if (itb == null) + { + String message = "nullValue.IndexedTriangleArray"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int numCoords = 2 * itb.vertexCount; + + if (texCoords == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (texCoords.capacity() < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + texCoords.capacity(); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.makeUnitSphereTextureCoordinates(itb.getVertexCount(), itb.getVertices(), + texCoords, seamVerticesIndex); + } + + public void makeUnitSphereTextureCoordinates(int vertexCount, FloatBuffer vertices, + FloatBuffer texCoords, int seamVerticesIndex) + { + if (vertices == null) + { + String message = "nullValue.VertexArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (vertices.capacity() < 3 * vertexCount) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "vertices.length=" + vertices.capacity()); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (texCoords == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (texCoords.capacity() < 2 * vertexCount) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "dest.length=" + texCoords.capacity()); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int i; + double x, y, z; + double theta, phi, u, v; + + // compute uv texture coordinates for each vertex and place them in the texCoords buffer. + for (i = 0; i < vertexCount; i++) + { + x = vertices.get(3 * i); + y = vertices.get(3 * i + 1); + z = vertices.get(3 * i + 2); + + phi = Math.atan2(y, x); + theta = Math.acos(z); + + if (phi < 0.0d) + phi += 2.0d * Math.PI; // shift phi to be in [0, 2*PI] + + u = phi / (2.0d * Math.PI); + v = (Math.PI - theta) / Math.PI; + + texCoords.put(2 * i, (float) u); + texCoords.put(2 * i + 1, (float) v); + } + + if (seamVerticesIndex > 0) // if the seam of the sphere was fixed + { + for (i = seamVerticesIndex; i < vertexCount; i++) + { + // wrap u (phi) texCoord for all the duplicated vertices + u = texCoords.get(2 * i); + if (u < 0.5) + texCoords.put(2 * i, (float) u + 1); + else + texCoords.put(2 * i, (float) u - 1); + } + } + texCoords.rewind(); + } + + // single texture version + public void makeUnitBoxTextureCoordinates(FloatBuffer texCoords, int vertexCount) + { + if (texCoords == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (texCoords.capacity() < 2 * vertexCount) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "dest.length=" + texCoords.capacity()); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // create uv texture coordinates for each of the 6 box faces and place them in the texCoords buffer. + for (int i = 0; i < vertexCount; i += 4) + { + // V0 (upper left) + texCoords.put(2 * i, 0); + texCoords.put(2 * i + 1, 1); + // V1 (upper right) + texCoords.put(2 * i + 2, 1); + texCoords.put(2 * i + 3, 1); + // V2 (lower left) + texCoords.put(2 * i + 4, 0); + texCoords.put(2 * i + 5, 0); + // V3 (lower right) + texCoords.put(2 * i + 6, 1); + texCoords.put(2 * i + 7, 0); + } + + texCoords.rewind(); + } + + // multi-texture version + public void makeUnitBoxTextureCoordinates(int index, FloatBuffer texCoords, int vertexCount) + { + if (texCoords == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (texCoords.capacity() < 2 * vertexCount) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "dest.length=" + texCoords.capacity()); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // create uv texture coordinates for each of the 6 box faces and place them in the texCoords buffer. + for (int i = 0; i < vertexCount; i += 4) + { + // V0 (upper left) + texCoords.put(2 * i, 0); + texCoords.put(2 * i + 1, 1); + // V1 (upper right) + texCoords.put(2 * i + 2, 1); + texCoords.put(2 * i + 3, 1); + // V2 (lower left) + texCoords.put(2 * i + 4, 0); + texCoords.put(2 * i + 5, 0); + // V3 (lower right) + texCoords.put(2 * i + 6, 1); + texCoords.put(2 * i + 7, 0); + } + + texCoords.rewind(); + } + + public void makeUnitPyramidTextureCoordinates(FloatBuffer texCoords, int vertexCount) + { + if (texCoords == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (texCoords.capacity() < 2 * vertexCount) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "dest.length=" + texCoords.capacity()); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // create uv texture coordinates for each of the 4 pyramid faces and for the base, and place them + // in the texCoords buffer. + + int i; + for (i = 0; i < vertexCount - 4; i += 3) // create texture coords for the 4 sides of the pyramid first + { + // V0 (point) + texCoords.put(2 * i, 0.5f); + texCoords.put(2 * i + 1, 1); + // V1 (lower left) + texCoords.put(2 * i + 2, 0); + texCoords.put(2 * i + 3, 0); + // V2 (lower right) + texCoords.put(2 * i + 4, 1); + texCoords.put(2 * i + 5, 0); + } + + // then create coords for the base + + // V0 (upper left) + texCoords.put(2 * i, 0); + texCoords.put(2 * i + 1, 1); + // V1 (upper right) + texCoords.put(2 * i + 2, 1); + texCoords.put(2 * i + 3, 1); + // V2 (lower left) + texCoords.put(2 * i + 4, 0); + texCoords.put(2 * i + 5, 0); + // V3 (lower right) + texCoords.put(2 * i + 6, 1); + texCoords.put(2 * i + 7, 0); + + texCoords.rewind(); + } + + public void makeUnitPyramidTextureCoordinates(int index, FloatBuffer texCoords, int vertexCount) + { + if (texCoords == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (texCoords.capacity() < 2 * vertexCount) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "dest.length=" + texCoords.capacity()); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // create uv texture coordinates for either one of the 4 pyramid faces or for the base, and place them + // in the texCoords buffer. + + int i = 0; + if (index == 4) // pyramid base + { + // V0 (upper left) + texCoords.put(2 * i, 0); + texCoords.put(2 * i + 1, 1); + // V1 (upper right) + texCoords.put(2 * i + 2, 1); + texCoords.put(2 * i + 3, 1); + // V2 (lower left) + texCoords.put(2 * i + 4, 0); + texCoords.put(2 * i + 5, 0); + // V3 (lower right) + texCoords.put(2 * i + 6, 1); + texCoords.put(2 * i + 7, 0); + } + else // pyramid side + { + for (i = 0; i < vertexCount; i += 3) // create texture coords for the 4 sides of the pyramid first + { + // V0 (point) + texCoords.put(2 * i, 0.5f); + texCoords.put(2 * i + 1, 1); + // V1 (lower left) + texCoords.put(2 * i + 2, 0); + texCoords.put(2 * i + 3, 0); + // V2 (lower right) + texCoords.put(2 * i + 4, 1); + texCoords.put(2 * i + 5, 0); + } + } + + texCoords.rewind(); + } + + public void makeUnitCylinderTextureCoordinates(int face, FloatBuffer texCoords, int subdivisions) + { + if (texCoords == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (subdivisions < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "subdivisions < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // create uv texture coordinates for the cylinder top, bottom and core, and place them + // in the texCoords buffer. + + int i, index; + float x, y, z, u, v, a, phi; + + int slices = (int) Math.pow(2, 2 + subdivisions); + float da = 2.0f * (float) Math.PI / (float) slices; + + if (face == 2) // cylinder core + { + int coreTexIndex = 0; + + for (i = 0; i < slices; i++) + { + a = i * da; + + // cylinder core top rim + u = 1 - a / (float) (2 * Math.PI); + + texCoords.put(coreTexIndex, u); + texCoords.put(coreTexIndex + 1, 1); + + // cylinder core bottom rim + texCoords.put(coreTexIndex + 2, u); + texCoords.put(coreTexIndex + 3, 0); + + coreTexIndex += 4; + } + + // close the texture seam + texCoords.put(coreTexIndex, 0); + texCoords.put(coreTexIndex + 1, 1); + + texCoords.put(coreTexIndex + 2, 0); + texCoords.put(coreTexIndex + 3, 0); + } + else // cylinder top or bottom + { + // center point + texCoords.put(0, 0.5f); + texCoords.put(1, 0.5f); + + // perimeter points + for (i = 0; i < slices; i++) + { + a = i * da; + x = (float) Math.sin(a); + y = (float) Math.cos(a); + + u = x / 2 + 0.5f; + v = y / 2 + 0.5f; + + if (face == 1) // Cylinder bottom + u = 1 - u; + + texCoords.put(2 * (i + 1), u); + texCoords.put(2 * (i + 1) + 1, v); + } + } + + texCoords.rewind(); + } + + public void makeWedgeTextureCoordinates(FloatBuffer texCoords, int subdivisions, Angle angle) + { + if (texCoords == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (subdivisions < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "subdivisions < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // create uv texture coordinates for the wedge top, bottom, core and sides, and place them + // in the texCoords buffer. + + int i, index; + float x, y, u, v, a; + + int slices = (int) Math.pow(2, 2 + subdivisions); + float da = (float) angle.getRadians() / slices; + int coreTexIndex = 4 * (slices + 2); + + // center points + texCoords.put(0, 0.5f); + texCoords.put(1, 0.5f); + + texCoords.put(2 * (slices + 2), 0.5f); + texCoords.put(2 * (slices + 2) + 1, 0.5f); + + for (i = 0; i <= slices; i++) + { + a = i * da; + x = (float) Math.sin(a); + y = (float) Math.cos(a); + + u = x / 2 + 0.5f; + v = y / 2 + 0.5f; + + // wedge top + texCoords.put(2 * (i + 1), u); + texCoords.put(2 * (i + 1) + 1, v); + + // wedge bottom + texCoords.put(2 * (slices + i + 3), 1 - u); + texCoords.put(2 * (slices + i + 3) + 1, v); + + // wedge core top rim + u = 1 - a / (float) (2 * Math.PI); + + texCoords.put(coreTexIndex, u); + texCoords.put(coreTexIndex + 1, 1); + + // wedge core bottom rim + texCoords.put(coreTexIndex + 2, u); + texCoords.put(coreTexIndex + 3, 0); + + coreTexIndex += 4; + } + + // wedge sides + for (i = 0; i < 2; i++) + { + index = 2 * (4 * (slices + 1 + i) + 2); + + // inner points + texCoords.put(index, 0); + texCoords.put(index + 1, 1); + + texCoords.put(index + 2, 0); + texCoords.put(index + 3, 0); + + // outer points + texCoords.put(index + 4, 1); + texCoords.put(index + 5, 1); + + texCoords.put(index + 6, 1); + texCoords.put(index + 7, 0); + } + + texCoords.rewind(); + } + + public void makeUnitWedgeTextureCoordinates(int face, FloatBuffer texCoords, int subdivisions, Angle angle) + { + if (texCoords == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (subdivisions < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "subdivisions < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // create uv texture coordinates for the wedge top, bottom, core and sides, and place them + // in the texCoords buffer. + + int i, index; + float x, y, u, v, a; + + // face 0 = top + // face 1 = bottom + // face 2 = round core wall + // face 3 = first wedge side + // face 4 = second wedge side + + int slices = (int) Math.pow(2, 2 + subdivisions); + float da = (float) angle.getRadians() / slices; + + if (face == 0 || face == 1) // wedge top or bottom + { + // center point + texCoords.put(0, 0.5f); + texCoords.put(1, 0.5f); + + for (i = 0; i <= slices; i++) + { + a = i * da; + x = (float) Math.sin(a); + y = (float) Math.cos(a); + + u = x / 2 + 0.5f; + v = y / 2 + 0.5f; + + if (face == 1) // wedge bottom + u = 1 - u; + + // rim point + texCoords.put(2 * (i + 1), u); + texCoords.put(2 * (i + 1) + 1, v); + } + } + else if (face == 2) // wedge core + { + int coreTexIndex = 0; + + for (i = 0; i <= slices; i++) + { + a = i * da; + + // cylinder core top rim + u = 1 - a / (float) (2 * Math.PI); + + texCoords.put(coreTexIndex, u); + texCoords.put(coreTexIndex + 1, 1); + + // cylinder core bottom rim + texCoords.put(coreTexIndex + 2, u); + texCoords.put(coreTexIndex + 3, 0); + + coreTexIndex += 4; + } + } + else if (face == 3) // west-facing wedge side + { + // inner points + texCoords.put(0, 1); + texCoords.put(1, 1); + + texCoords.put(2, 1); + texCoords.put(3, 0); + + // outer points + texCoords.put(4, 0); + texCoords.put(5, 1); + + texCoords.put(6, 0); + texCoords.put(7, 0); + } + else if (face == 4) // adjustable wedge side + { + // inner points + texCoords.put(0, 0); + texCoords.put(1, 1); + + texCoords.put(2, 0); + texCoords.put(3, 0); + + // outer points + texCoords.put(4, 1); + texCoords.put(5, 1); + + texCoords.put(6, 1); + texCoords.put(7, 0); + } + + texCoords.rewind(); + } + + public void makeUnitConeTextureCoordinates(FloatBuffer texCoords, int subdivisions) + { + if (texCoords == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (subdivisions < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "subdivisions < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // create uv texture coordinates for the cone bottom and core, and place them + // in the texCoords buffer. + + int i, index; + float x, y, z, u, v, a, phi; + + int slices = (int) Math.pow(2, 2 + subdivisions); + float da = 2.0f * (float) Math.PI / (float) slices; + int coreTexIndex = 2 * (slices + 1); + + // center point + texCoords.put(0, 0.5f); + texCoords.put(1, 0.5f); + + for (i = 0; i < slices; i++) + { + a = i * da; + x = (float) Math.sin(a); + y = (float) Math.cos(a); + + u = x / 2 + 0.5f; + v = y / 2 + 0.5f; + + // cone bottom + texCoords.put(2 * (i + 1), 1 - u); + texCoords.put(2 * (i + 1) + 1, v); + + // cone core top rim + u = 1 - a / (float) (2 * Math.PI); + + texCoords.put(coreTexIndex, u); + texCoords.put(coreTexIndex + 1, 1); + + // cone core bottom rim + texCoords.put(coreTexIndex + 2, u); + texCoords.put(coreTexIndex + 3, 0); + + coreTexIndex += 4; + } + + // close the texture seam + texCoords.put(coreTexIndex, 0); + texCoords.put(coreTexIndex + 1, 1); + + texCoords.put(coreTexIndex + 2, 0); + texCoords.put(coreTexIndex + 3, 0); + + texCoords.rewind(); + } + + public void makeUnitConeTextureCoordinates(int face, FloatBuffer texCoords, int subdivisions) + { + if (texCoords == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (subdivisions < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "subdivisions < 0"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // create uv texture coordinates for the cone base and core, and place them + // in the texCoords buffer. + + int i, index; + float x, y, z, u, v, a, phi; + + int slices = (int) Math.pow(2, 2 + subdivisions); + float da = 2.0f * (float) Math.PI / (float) slices; + + if (face == 1) // cone core + { + int coreTexIndex = 0; + + for (i = 0; i < slices; i++) + { + a = i * da; + + // cone core top rim + u = 1 - a / (float) (2 * Math.PI); + + texCoords.put(coreTexIndex, u); + texCoords.put(coreTexIndex + 1, 1); + + // core bottom rim + texCoords.put(coreTexIndex + 2, u); + texCoords.put(coreTexIndex + 3, 0); + + coreTexIndex += 4; + } + + // close the texture seam + texCoords.put(coreTexIndex, 0); + texCoords.put(coreTexIndex + 1, 1); + + texCoords.put(coreTexIndex + 2, 0); + texCoords.put(coreTexIndex + 3, 0); + } + else if (face == 0) // cone base + { + // center point + texCoords.put(0, 0.5f); + texCoords.put(1, 0.5f); + + // perimeter points + for (i = 0; i < slices; i++) + { + a = i * da; + x = (float) Math.sin(a); + y = (float) Math.cos(a); + + u = x / 2 + 0.5f; + v = y / 2 + 0.5f; + + texCoords.put(2 * (i + 1), 1 - u); + texCoords.put(2 * (i + 1) + 1, v); + } + } + + texCoords.rewind(); + } + + //**************************************************************// + //******************** Indexed Triangle Array ****************// + //**************************************************************// + + public int getIndexedTriangleArrayDrawMode() + { + return GL_TRIANGLES; + } + + public static class IndexedTriangleArray + { + private int indexCount; + private int vertexCount; + private int[] indices; + private float[] vertices; + + public IndexedTriangleArray(int indexCount, int[] indices, int vertexCount, float[] vertices) + { + this.indexCount = indexCount; + this.indices = indices; + this.vertexCount = vertexCount; + this.vertices = vertices; + } + + public int getIndexCount() + { + return this.indexCount; + } + + public int[] getIndices() + { + return this.indices; + } + + public int getVertexCount() + { + return this.vertexCount; + } + + public float[] getVertices() + { + return this.vertices; + } + } + + public void subdivideIndexedTriangleArray(IndexedTriangleArray ita) + { + if (ita == null) + { + String message = "nullValue.IndexedTriangleArray"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int indexCount; + int a, b, c; + int ab, bc, ca; + int i, j; + HashMap edgeMap; + Edge e; + Integer split; + + indexCount = ita.indexCount; + edgeMap = new HashMap(); + + // Iterate over each triangle, and split the edge of each triangle. Each edge is split exactly once. The + // index of the new vertex created by a split is stored in edgeMap. + for (i = 0; i < indexCount; i += 3) + { + for (j = 0; j < 3; j++) + { + a = ita.indices[i + j]; + b = ita.indices[(j < 2) ? (i + j + 1) : i]; + e = new Edge(a, b); + split = edgeMap.get(e); + if (split == null) + { + split = this.splitVertex(ita, a, b); + edgeMap.put(e, split); + } + } + } + + // Iterate over each triangle, and create indices for four new triangles, replacing indices of the original + // triangle. + for (i = 0; i < indexCount; i += 3) + { + a = ita.indices[i]; + b = ita.indices[i + 1]; + c = ita.indices[i + 2]; + ab = edgeMap.get(new Edge(a, b)); + bc = edgeMap.get(new Edge(b, c)); + ca = edgeMap.get(new Edge(c, a)); + this.indexSplitTriangle(ita, i, a, b, c, ab, bc, ca); + } + } + + public IndexedTriangleArray subdivideIndexedTriangles(int indexCount, int[] indices, + int vertexCount, float[] vertices) + { + int numCoords = 3 * vertexCount; + + if (indices == null) + { + String message = "nullValue.IndexArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (indices.length < indexCount) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "indices.length=" + indices.length); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (vertices == null) + { + String message = "nullValue.VertexArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (vertices.length < numCoords) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "vertices.length=" + vertices.length); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + IndexedTriangleArray ita = new IndexedTriangleArray(indexCount, indices, vertexCount, vertices); + this.subdivideIndexedTriangleArray(ita); + + return ita; + } + + public void makeIndexedTriangleArrayNormals(IndexedTriangleArray ita, float[] dest) + { + if (ita == null) + { + String message = "nullValue.IndexedTriangleArray"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int numCoords = 3 * ita.vertexCount; + + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.makeIndexedTriangleArrayNormals(0, ita.indexCount, ita.indices, 0, ita.vertexCount, ita.vertices, dest); + } + + public void makeIndexedTriangleArrayNormals(int indexPos, int indexCount, int[] indices, + int vertexPos, int vertexCount, float[] vertices, + float[] dest) + { + if (indices == null) + { + String message = "nullValue.IndexArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (indices.length < (indexPos + indexCount)) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "indices.length=" + indices.length); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (vertices == null) + { + String message = "nullValue.VertexArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (vertices.length < (vertexPos + vertexCount)) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "vertices.length=" + vertices.length); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < (vertexPos + vertexCount)) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "dest.length=" + dest.length); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int i, v; + int index; + float nsign; + float[] norm; + int[] faceIndices; + + nsign = (this.orientation == OUTSIDE) ? 1.0f : -1.0f; + norm = new float[3]; + faceIndices = new int[3]; + + // Compute the normal for each face, contributing that normal to each vertex of the face. + for (i = 0; i < indexCount; i += 3) + { + faceIndices[0] = indices[indexPos + i]; + faceIndices[1] = indices[indexPos + i + 1]; + faceIndices[2] = indices[indexPos + i + 2]; + // Compute the normal for this face. + this.facenorm(vertices, faceIndices[0], faceIndices[1], faceIndices[2], norm); + // Add this face normal to the normal at each vertex. + for (v = 0; v < 3; v++) + { + index = 3 * faceIndices[v]; + this.add3AndSet(dest, index, norm, 0); + } + } + + // Scale and normalize each vertex normal. + for (v = 0; v < vertexCount; v++) + { + index = 3 * (vertexPos + v); + this.mul3AndSet(dest, index, nsign); + this.norm3AndSet(dest, index); + } + } + + public void makeIndexedTriangleStripNormals(int indexPos, int indexCount, int[] indices, + int vertexPos, int vertexCount, float[] vertices, + float[] dest) + { + if (indices == null) + { + String message = "nullValue.IndexArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (indices.length < indexPos + indexCount) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "indices.length=" + indices.length); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (vertices == null) + { + String message = "nullValue.VertexArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (vertices.length < 3 * (vertexPos + vertexCount)) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "vertices.length=" + vertices.length); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < 3 * (vertexPos + vertexCount)) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "dest.length=" + dest.length); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int i, v; + int index; + float nsign; + float[] norm; + int[] faceIndices; + + nsign = (this.orientation == OUTSIDE) ? 1.0f : -1.0f; + norm = new float[3]; + faceIndices = new int[3]; + + // Compute the normal for each face, contributing that normal to each vertex of the face. + for (i = 2; i < indexCount; i++) + { + if ((i % 2) == 0) + { + faceIndices[0] = indices[indexPos + i - 2]; + faceIndices[1] = indices[indexPos + i - 1]; + faceIndices[2] = indices[indexPos + i]; + } + else + { + faceIndices[0] = indices[indexPos + i - 1]; + faceIndices[1] = indices[indexPos + i - 2]; + faceIndices[2] = indices[indexPos + i]; + } + // Compute the normal for this face. + this.facenorm(vertices, faceIndices[0], faceIndices[1], faceIndices[2], norm); + // Add this face normal to the normal at each vertex. + for (v = 0; v < 3; v++) + { + index = 3 * faceIndices[v]; + this.add3AndSet(dest, index, norm, 0); + } + } + + // Scale and normalize each vertex normal. + for (v = 0; v < vertexCount; v++) + { + index = 3 * (vertexPos + v); + this.mul3AndSet(dest, index, nsign); + this.norm3AndSet(dest, index); + } + } + + private int splitVertex(IndexedTriangleArray ita, int a, int b) + { + int minCapacity, oldCapacity, newCapacity; + + oldCapacity = ita.vertices.length; + minCapacity = 3 * (ita.vertexCount + 1); + while (minCapacity > oldCapacity) + { + newCapacity = 2 * oldCapacity; + ita.vertices = this.copyOf(ita.vertices, newCapacity); + oldCapacity = newCapacity; + } + + int s = ita.vertexCount; + int is = 3 * s; + int ia = 3 * a; + int ib = 3 * b; + ita.vertices[is] = (ita.vertices[ia] + ita.vertices[ib]) / 2.0f; + ita.vertices[is + 1] = (ita.vertices[ia + 1] + ita.vertices[ib + 1]) / 2.0f; + ita.vertices[is + 2] = (ita.vertices[ia + 2] + ita.vertices[ib + 2]) / 2.0f; + ita.vertexCount++; + + return s; + } + + private void indexSplitTriangle(IndexedTriangleArray ita, int original, int a, int b, int c, int ab, int bc, int ca) + { + int minCapacity, oldCapacity, newCapacity; + + // One of the new triangles will overwrite the original triangles, so we only need enough space to index + // three new triangles. + oldCapacity = ita.indices.length; + minCapacity = ita.indexCount + 9; + while (minCapacity > oldCapacity) + { + newCapacity = 2 * oldCapacity; + ita.indices = this.copyOf(ita.indices, newCapacity); + oldCapacity = newCapacity; + } + + // Lower-left triangle. + // This triangle replaces the original. + ita.indices[original] = a; + ita.indices[original + 1] = ab; + ita.indices[original + 2] = ca; + + // Center triangle. + ita.indices[ita.indexCount++] = ab; + ita.indices[ita.indexCount++] = bc; + ita.indices[ita.indexCount++] = ca; + + // Lower-right triangle. + ita.indices[ita.indexCount++] = ab; + ita.indices[ita.indexCount++] = b; + ita.indices[ita.indexCount++] = bc; + + // Upper triangle. + ita.indices[ita.indexCount++] = ca; + ita.indices[ita.indexCount++] = bc; + ita.indices[ita.indexCount++] = c; + } + + private static class Edge + { + public final int a; + public final int b; + + public Edge(int a, int b) + { + this.a = a; + this.b = b; + } + + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + // Compares a non directed edge between two points. Therefore we must treat edge equivalence as + // edge(ab)=edge(ab) OR edge(ab)=edge(ba). + Edge that = (Edge) o; + return (this.a == that.a && this.b == that.b) + || (this.a == that.b && this.b == that.a); + } + + public int hashCode() + { + // Represents the hash for a a non directed edge between two points. Therefore we use a non-commutative + // hash so that hash(ab)=hash(ba). + return this.a + this.b; + } + } + + //**************************************************************// + //******************** Subdivision Points ********************// + //**************************************************************// + + public int getSubdivisionPointsVertexCount(int subdivisions) + { + return (1 << subdivisions) + 1; + } + + public void makeSubdivisionPoints(float x1, float y1, float z1, float x2, float y2, float z2, + int subdivisions, float[] dest) + { + int numPoints = this.getSubdivisionPointsVertexCount(subdivisions); + int numCoords = 3 * numPoints; + + if (numPoints < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "subdivisions=" + subdivisions); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < numCoords) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int first, last; + int index; + + first = 0; + last = numPoints - 1; + + index = 3 * first; + dest[index] = x1; + dest[index + 1] = y1; + dest[index + 2] = z1; + + index = 3 * last; + dest[index] = x2; + dest[index + 1] = y2; + dest[index + 2] = z2; + + this.subdivide(x1, y1, z1, x2, y2, z2, subdivisions, dest, first, last); + } + + private void subdivide(float x1, float y1, float z1, float x2, float y2, float z2, int subdivisions, + float[] dest, int first, int last) + { + float x, y, z; + int mid, index; + + if (subdivisions <= 0) + return; + + x = (x1 + x2) / 2.0f; + y = (y1 + y2) / 2.0f; + z = (z1 + z2) / 2.0f; + + mid = (first + last) / 2; + index = mid * 3; + dest[index] = x; + dest[index + 1] = y; + dest[index + 2] = z; + + if (subdivisions > 1) + { + this.subdivide(x1, y1, z1, x, y, z, subdivisions - 1, dest, first, mid); + this.subdivide(x, y, z, x2, y2, z2, subdivisions - 1, dest, mid, last); + } + } + + //**************************************************************// + //******************** Bilinear Surface ********************// + //**************************************************************// + + public int getBilinearSurfaceFillIndexCount(int uStacks, int vStacks) + { + return vStacks * 2 * (uStacks + 1) + 2 * (vStacks - 1); + } + + public int getBilinearSurfaceOutlineIndexCount(int uStacks, int vStacks, int mask) + { + int count = 0; + if ((mask & TOP) != 0) + count += 2 * uStacks; + if ((mask & BOTTOM) != 0) + count += 2 * uStacks; + if ((mask & LEFT) != 0) + count += 2 * vStacks; + if ((mask & RIGHT) != 0) + count += 2 * vStacks; + + return count; + } + + public int getBilinearSurfaceVertexCount(int uStacks, int vStacks) + { + return (uStacks + 1) * (vStacks + 1); + } + + public int getBilinearSurfaceFillDrawMode() + { + return GL_TRIANGLE_STRIP; + } + + public int getBilinearSurfaceOutlineDrawMode() + { + return GL_LINES; + } + + public void makeBilinearSurfaceFillIndices(int vertexPos, int uStacks, int vStacks, int destPos, int[] dest) + { + int numIndices = this.getBilinearSurfaceFillIndexCount(uStacks, vStacks); + + if (numIndices < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "uStacks=" + uStacks + + " vStacks=" + vStacks); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < (numIndices + destPos)) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int ui, vi; + int vertex, index; + + index = destPos; + for (vi = 0; vi < vStacks; vi++) + { + if (vi != 0) + { + if (this.orientation == INSIDE) + { + vertex = uStacks + vi * (uStacks + 1); + dest[index++] = vertexPos + vertex; + vertex = vi * (uStacks + 1); + dest[index++] = vertexPos + vertex; + } + else // (this.orientation == OUTSIDE) + { + vertex = uStacks + (vi - 1) * (uStacks + 1); + dest[index++] = vertexPos + vertex; + vertex = vi * (uStacks + 1) + (uStacks + 1); + dest[index++] = vertexPos + vertex; + } + } + for (ui = 0; ui <= uStacks; ui++) + { + vertex = ui + vi * (uStacks + 1); + if (this.orientation == INSIDE) + { + dest[index++] = vertexPos + vertex; + dest[index++] = vertexPos + vertex + (uStacks + 1); + } + else // (this.orientation == OUTSIDE) + { + dest[index++] = vertexPos + vertex + (uStacks + 1); + dest[index++] = vertexPos + vertex; + } + } + } + } + + public void makeBilinearSurfaceOutlineIndices(int vertexPos, int uStacks, int vStacks, int mask, int destPos, + int[] dest) + { + int numIndices = this.getBilinearSurfaceOutlineIndexCount(uStacks, vStacks, mask); + + if (numIndices < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "uStacks=" + uStacks + + " vStacks=" + vStacks); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < (numIndices + destPos)) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int ui, vi; + int vertex, index; + + index = destPos; + // Bottom row. + if ((mask & BOTTOM) != 0) + { + for (ui = 0; ui < uStacks; ui++) + { + vertex = ui; + dest[index++] = vertexPos + vertex; + vertex = ui + 1; + dest[index++] = vertexPos + vertex; + } + } + // Right side. + if ((mask & RIGHT) != 0) + { + for (vi = 0; vi < vStacks; vi++) + { + vertex = uStacks + vi * (uStacks + 1); + dest[index++] = vertexPos + vertex; + vertex = uStacks + (vi + 1) * (uStacks + 1); + dest[index++] = vertexPos + vertex; + } + } + // Top side. + if ((mask & TOP) != 0) + { + for (ui = uStacks; ui > 0; ui--) + { + vertex = ui + vStacks * (uStacks + 1); + dest[index++] = vertexPos + vertex; + vertex = (ui - 1) + vStacks * (uStacks + 1); + dest[index++] = vertexPos + vertex; + } + } + // Left side. + if ((mask & LEFT) != 0) + { + for (vi = vStacks; vi > 0; vi--) + { + vertex = vi * (uStacks + 1); + dest[index++] = vertexPos + vertex; + vertex = (vi - 1) * (uStacks + 1); + dest[index++] = vertexPos + vertex; + } + } + } + + public void makeBilinearSurfaceVertices(float[] control, int destPos, int uStacks, int vStacks, float[] dest) + { + int numPoints = this.getBilinearSurfaceVertexCount(uStacks, vStacks); + int numCoords = 3 * numPoints; + + if (control == null) + { + String message = "nullValue.ControlPointArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (control.length < 12) + { + String message = "generic.ControlPointArrayInvalidLength " + control.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (numPoints < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "uStacks=" + uStacks + + " vStacks=" + vStacks); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < (numCoords + 3 * destPos)) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + float x, y, z; + float u, v; + float du, dv; + float oneMinusU, oneMinusV; + int ui, vi; + int index; + + du = 1.0f / (float) uStacks; + dv = 1.0f / (float) vStacks; + + for (vi = 0; vi <= vStacks; vi++) + { + v = vi * dv; + oneMinusV = 1.0f - v; + for (ui = 0; ui <= uStacks; ui++) + { + u = ui * du; + oneMinusU = 1.0f - u; + index = ui + vi * (uStacks + 1); + index = 3 * (destPos + index); + x = oneMinusU * oneMinusV * control[0] // Lower left control point + + u * oneMinusV * control[3] // Lower right control point + + u * v * control[6] // Upper right control point + + oneMinusU * v * control[9]; // Upper left control point + y = oneMinusU * oneMinusV * control[1] + + u * oneMinusV * control[4] + + u * v * control[7] + + oneMinusU * v * control[10]; + z = oneMinusU * oneMinusV * control[2] + + u * oneMinusV * control[5] + + u * v * control[8] + + oneMinusU * v * control[11]; + dest[index] = x; + dest[index + 1] = y; + dest[index + 2] = z; + } + } + } + + public void makeBilinearSurfaceVertexNormals(int srcPos, int uStacks, int vStacks, float[] srcVerts, + int destPos, float dest[]) + { + int numPoints = this.getBilinearSurfaceVertexCount(uStacks, vStacks); + int numCoords = 3 * numPoints; + + if (numPoints < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "uStacks=" + uStacks + + " vStacks=" + vStacks); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (srcVerts == null) + { + String message = "nullValue.SourceVertexArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest == null) + { + String message = "nullValue.DestinationArrayIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dest.length < (numCoords + 3 * destPos)) + { + String message = "generic.DestinationArrayInvalidLength " + dest.length; + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int ui, vi; + int index; + int vprev, vnext; + float nsign; + float[] norm, zero, tmp; + + nsign = (this.orientation == OUTSIDE) ? 1.0f : -1.0f; + norm = new float[3]; + zero = new float[3]; + tmp = new float[3]; + + for (vi = 0; vi <= vStacks; vi++) + { + for (ui = 0; ui <= uStacks; ui++) + { + index = ui + vi * (uStacks + 1); + index = srcPos + index; + vprev = index - (uStacks + 1); + vnext = index + (uStacks + 1); + + System.arraycopy(zero, 0, norm, 0, 3); + + // Adjacent faces below. + if (vi > 0) + { + // Adjacent faces below and to the left. + if (ui > 0) + { + this.facenorm(srcVerts, index, index - 1, vprev - 1, tmp); + this.add3AndSet(norm, 0, tmp, 0); + this.facenorm(srcVerts, index, vprev - 1, vprev, tmp); + this.add3AndSet(norm, 0, tmp, 0); + } + // Adjacent faces below and to the right. + if (ui < uStacks) + { + this.facenorm(srcVerts, index, vprev, vprev + 1, tmp); + this.add3AndSet(norm, 0, tmp, 0); + this.facenorm(srcVerts, index, vprev + 1, index + 1, tmp); + this.add3AndSet(norm, 0, tmp, 0); + } + } + + // Adjacent faces above. + if (vi < vStacks) + { + // Adjacent faces above and to the left. + if (ui > 0) + { + this.facenorm(srcVerts, index, vnext, vnext - 1, tmp); + this.add3AndSet(norm, 0, tmp, 0); + this.facenorm(srcVerts, index, vnext - 1, index - 1, tmp); + this.add3AndSet(norm, 0, tmp, 0); + } + // Adjacent faces above and to the right. + if (ui < uStacks) + { + this.facenorm(srcVerts, index, index + 1, vnext + 1, tmp); + this.add3AndSet(norm, 0, tmp, 0); + this.facenorm(srcVerts, index, vnext + 1, vnext, tmp); + this.add3AndSet(norm, 0, tmp, 0); + } + } + + // Normalize and place in output. + this.mul3AndSet(norm, 0, nsign); + this.norm3AndSet(norm, 0); + System.arraycopy(norm, 0, dest, 3 * index, 3); + } + } + } + + //**************************************************************// + //******************** 2D Shapes *****************************// + //**************************************************************// + + /** + * Creates a vertex buffer for a two-dimensional ellipse centered at the specified location and with the specified + * radii. The ellipse's center is placed at (x, y), it has a width of 2 * majorRadius, and + * a height of 2 * minorRadius. + *

    + * If the specified slices is greater than 1 this returns a buffer with vertices evenly spaced along + * the circumference of the ellipse. Otherwise this returns a buffer with one vertex. + *

    + * The returned buffer contains pairs of xy coordinates representing the location of each vertex in the ellipse in a + * counter-clockwise winding order relative to the z axis. The buffer may be rendered in OpenGL as either a triangle + * fan or a line loop. + * + * @param x the x-coordinate of the ellipse's center. + * @param y the y-coordinate of the ellipse's center. + * @param majorRadius the ellipse's radius along the x axis. + * @param minorRadius the ellipse's radius along the y axis. + * @param slices the number of slices in the ellipse. + * + * @return a buffer containing the ellipse's x and y locations. + * + * @throws IllegalArgumentException if any of majorRadius, minorRadius, or + * slices are less than zero. + */ + public FloatBuffer makeEllipse(float x, float y, float majorRadius, float minorRadius, int slices) + { + if (majorRadius < 0) + { + String message = Logging.getMessage("Geom.RadiusIsNegative"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (minorRadius < 0) + { + String message = Logging.getMessage("Geom.RadiusIsNegative"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (slices < 0) + { + String message = Logging.getMessage("generic.NumSlicesIsNegative"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // Return a buffer with only the first point at angle 0 if the number of slices is zero or one. + if (slices <= 1) + { + // The buffer contains one coordinate pair. + FloatBuffer buffer = BufferUtil.newFloatBuffer(2); + buffer.put(x + majorRadius); + buffer.put(y); + buffer.rewind(); + return buffer; + } + + float step = (float) Math.PI * 2f / (float) slices; + float angle = 0; + + // The buffer contains one coordinate pair per slice. + FloatBuffer buffer = BufferUtil.newFloatBuffer(2 * slices); + + // Add each vertex on the circumference of the ellipse, starting at zero and ending one step before 360. + for (int i = 0; i < slices; i++, angle += step) + { + buffer.put(x + (float) Math.cos(angle) * majorRadius); + buffer.put(y + (float) Math.sin(angle) * minorRadius); + } + + // Rewind and return. + buffer.rewind(); + return buffer; + } + + /** + * Creates a vertex buffer for a two-dimensional ellipse centered at the specified location and with the specified + * radii. The ellipse's center is placed at (x, y), it has a width of 2 * majorRadius, and + * a height of 2 * minorRadius. + *

    + * If the specified slices is greater than 1 this returns a buffer with vertices evenly spaced along + * the circumference of the ellipse. Otherwise this returns a buffer with one vertex. + *

    + * If the specified leaderWidth is greater than zero and the location (leaderX, leaderY) + * is outside of the rectangle that encloses the ellipse, the ellipse has a triangle attached to one side with with + * its top pointing at (leaderX, leaderY). Otherwise this returns an ellipse with no leader and is + * equivalent to calling {@link #makeEllipse(float, float, float, float, int)}. The leader is attached + * at the center of either the top, bottom, left, or right side, depending on the leader's location relative to the + * ellipse. The leader width is limited in size by the side it is attached to. For example, if the leader is + * attached to the ellipse's bottom, its width is limited by the ellipse's major radius. + * + * @param x the x-coordinate of the ellipse's center. + * @param y the y-coordinate of the ellipse's center. + * @param majorRadius the ellipse's radius along the x axis. + * @param minorRadius the ellipse's radius along the y axis. + * @param slices the number of slices in the ellipse. + * @param leaderX the x-coordinate the leader points to. + * @param leaderY the y-coordinate the leader points to. + * @param leaderWidth the leader triangle's width. + * + * @return a buffer containing the ellipse's x and y locations. + * + * @throws IllegalArgumentException if any of majorRadius, minorRadius, + * slices, or leaderWidth are less than zero. + */ + public FloatBuffer makeEllipseWithLeader(float x, float y, float majorRadius, float minorRadius, int slices, + float leaderX, float leaderY, float leaderWidth) + { + if (majorRadius < 0) + { + String message = Logging.getMessage("Geom.RadiusIsNegative"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (minorRadius < 0) + { + String message = Logging.getMessage("Geom.RadiusIsNegative"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (slices < 0) + { + String message = Logging.getMessage("generic.NumSlicesIsNegative"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (leaderWidth < 0) + { + String message = Logging.getMessage("Geom.WidthIsNegative"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // Return an ellipse without a leader if the leader width is zero. + if (leaderWidth == 0) + return this.makeEllipse(x, y, majorRadius, minorRadius, slices); + + int leaderCode = this.computeLeaderLocationCode(x - majorRadius, y - minorRadius, x + majorRadius, + y + minorRadius, leaderX, leaderY); + + // Return an ellipse without a leader if the leader point is inside the rectangle. + if (leaderCode == LEADER_LOCATION_INSIDE) + return this.makeEllipse(x, y, majorRadius, minorRadius, slices); + + // Return a buffer with only the first point at angle 0 if the number of slices is zero or one. + if (slices <= 1) + { + // The buffer contains one coordinate pair. + FloatBuffer buffer = BufferUtil.newFloatBuffer(2); + buffer.put(x + majorRadius); + buffer.put(y); + buffer.rewind(); + return buffer; + } + + // Determine the leader's size in radians and the starting angle according to the leader's location relative to + // the ellipse. + float leaderAngle; + float startAngle; + + if ((leaderCode & LEADER_LOCATION_BOTTOM) != 0) + { + // Limit the leader's width by the ellipse's major radius. + float maxLeaderWidth = 2f * majorRadius; + if (leaderWidth > maxLeaderWidth) + leaderWidth = maxLeaderWidth; + + leaderAngle = leaderWidth / majorRadius; + startAngle = 3f * (float) Math.PI / 2f; + } + else if ((leaderCode & LEADER_LOCATION_TOP) != 0) + { + // Limit the leader's width by the ellipse's major radius. + float maxLeaderWidth = 2f * majorRadius; + if (leaderWidth > maxLeaderWidth) + leaderWidth = maxLeaderWidth; + + leaderAngle = leaderWidth / majorRadius; + startAngle = (float) Math.PI / 2f; + } + else if ((leaderCode & LEADER_LOCATION_LEFT) != 0) + { + // Limit the leader's width by the ellipse's minor radius. + float maxLeaderWidth = 2f * minorRadius; + if (leaderWidth > maxLeaderWidth) + leaderWidth = maxLeaderWidth; + + leaderAngle = leaderWidth / minorRadius; + startAngle = (float) Math.PI; + } + else if ((leaderCode & LEADER_LOCATION_RIGHT) != 0) + { + // Limit the leader's width by the ellipse's minor radius. + float maxLeaderWidth = 2f * minorRadius; + if (leaderWidth > maxLeaderWidth) + leaderWidth = maxLeaderWidth; + + leaderAngle = leaderWidth / minorRadius; + startAngle = 0f; + } + else + { + // Return an ellipse without a leader if the leader location code is unrecognized. This should never happen, + // but we check anyway. + return this.makeEllipse(x, y, majorRadius, minorRadius, slices); + } + + float step = (float) (Math.PI * 2f - leaderAngle) / (float) slices; + float angle = startAngle + leaderAngle / 2f; + + // The buffer contains one coordinate pair per slice, and three coordinate pairs for the leader. + FloatBuffer buffer = BufferUtil.newFloatBuffer(2 * slices + 6); + // Start in the leader right corner to ensure the vertices can be drawn as a triangle fan. + buffer.put(x + (float) Math.cos(startAngle + leaderAngle / 2f) * majorRadius); + buffer.put(y + (float) Math.sin(startAngle + leaderAngle / 2f) * minorRadius); + + // Add each vertex on the circumference of the ellipse, starting at the right side of the leader, and ending at + // the left side of the leader. + for (int i = 0; i < slices; i++, angle += step) + { + buffer.put(x + (float) Math.cos(angle) * majorRadius); + buffer.put(y + (float) Math.sin(angle) * minorRadius); + } + + // Leader left corner. + buffer.put(x + (float) Math.cos(startAngle - leaderAngle / 2f) * majorRadius); + buffer.put(y + (float) Math.sin(startAngle - leaderAngle / 2f) * minorRadius); + // Leader point. + buffer.put(leaderX); + buffer.put(leaderY); + + // Rewind and return. + buffer.rewind(); + return buffer; + } + + /** + * Creates a vertex buffer for a two-dimensional rectangle at the specified location, and with the specified size. + * The rectangle's lower left corner is placed at (x, y), and its upper right corner is placed at + * (x + width, y + height). + *

    + * The returned buffer contains pairs of xy coordinates representing the location of each vertex in the rectangle in + * a counter-clockwise winding order relative to the z axis. The buffer may be rendered in OpenGL as either a + * triangle fan or a line loop. + * + * @param x the x-coordinate of the rectangle's lower left corner. + * @param y the y-coordinate of the rectangle's lower left corner. + * @param width the rectangle's width. + * @param height the rectangle's height. + * + * @return a buffer containing the rectangle's x and y locations. + * + * @throws IllegalArgumentException if either width or height are less than zero. + */ + public FloatBuffer makeRectangle(float x, float y, float width, float height) + { + if (width < 0) + { + String message = Logging.getMessage("Geom.WidthIsNegative"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (height < 0) + { + String message = Logging.getMessage("Geom.HeightIsNegative"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // The buffer contains eight coordinate pairs: two pairs for each corner. + FloatBuffer buffer = BufferUtil.newFloatBuffer(8); + // Lower left corner. + buffer.put(x); + buffer.put(y); + // Lower right corner. + buffer.put(x + width); + buffer.put(y); + // Upper right corner. + buffer.put(x + width); + buffer.put(y + height); + // Upper left corner. + buffer.put(x); + buffer.put(y + height); + // Rewind and return. + buffer.rewind(); + return buffer; + } + + /** + * Creates a vertex buffer for a two-dimensional rectangle at the specified location, with the specified size, and + * with optionally rounded corners. The rectangle's lower left corner is placed at the (x, y), and its + * upper right corner is placed at (x + width, y + height). + *

    + * If the specified cornerRadius and cornerSlices are greater than 0, the rectangle's + * corners have a rounded appearance. The radius specifies the size of a rounded corner, and the slices specifies + * the number of segments that make a rounded corner. If either cornerRadius or + * cornerSlices are 0, this returns a rectangle with sharp corners and is equivalent to calling + * {@link #makeRectangle(float, float, float, float)}. The cornerRadius is limited by the + * rectangle's width and height. For example, if the corner radius is 100 and the width and height are 50 and 100, + * the actual corner radius used is 25 - half of the rectangle's smallest dimension. + *

    + * The returned buffer contains pairs of xy coordinates representing the location of each vertex in the rectangle in + * a counter-clockwise winding order relative to the z axis. The buffer may be rendered in OpenGL as either a + * triangle fan or a line loop. + * + * @param x the x-coordinate of the rectangle's lower left corner. + * @param y the y-coordinate of the rectangle's lower left corner. + * @param width the rectangle's width. + * @param height the rectangle's height. + * @param cornerRadius the rectangle's rounded corner radius, or 0 to disable rounded corners. + * @param cornerSlices the number of slices in each rounded corner, or 0 to disable rounded corners. + * + * @return a buffer containing the rectangle's x and y locations. + * + * @throws IllegalArgumentException if any of width, height, cornerRadius, or + * cornerSlices are less than zero. + */ + public FloatBuffer makeRectangle(float x, float y, float width, float height, float cornerRadius, int cornerSlices) + { + if (width < 0) + { + String message = Logging.getMessage("Geom.WidthIsNegative"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (height < 0) + { + String message = Logging.getMessage("Geom.HeightIsNegative"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (cornerRadius < 0) + { + String message = Logging.getMessage("Geom.RadiusIsNegative"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (cornerSlices < 0) + { + String message = Logging.getMessage("generic.NumSlicesIsNegative"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // Limit the corner radius to half of the rectangles width or height, whichever is smaller. + float maxCornerRadius = Math.min(width, height) / 2f; + if (cornerRadius > maxCornerRadius) + cornerRadius = maxCornerRadius; + + // Create a rectangle with sharp corners if either the corner radius or the number of corner slices is 0. + if (cornerRadius == 0f || cornerSlices == 0) + return this.makeRectangle(x, y, width, height); + + float piOver2 = (float) Math.PI / 2f; + + // The buffer contains four coordinate pairs for each corner, and two coordinate pairs per corner vertex. + FloatBuffer buffer = BufferUtil.newFloatBuffer(16 + 8 * (cornerSlices - 1)); + // Lower left corner. + buffer.put(x); + buffer.put(y + cornerRadius); + this.addRectangleRoundedCorner(x + cornerRadius, x + cornerRadius, cornerRadius, (float) Math.PI, piOver2, + cornerSlices, buffer); + buffer.put(x + cornerRadius); + buffer.put(y); + // Lower right corner. + buffer.put(x + width - cornerRadius); + buffer.put(y); + this.addRectangleRoundedCorner(x + width - cornerRadius, y + cornerRadius, cornerRadius, -piOver2, piOver2, + cornerSlices, buffer); + buffer.put(x + width); + buffer.put(y + cornerRadius); + // Upper right corner. + buffer.put(x + width); + buffer.put(y + height - cornerRadius); + this.addRectangleRoundedCorner(x + width - cornerRadius, y + height - cornerRadius, cornerRadius, 0f, piOver2, + cornerSlices, buffer); + buffer.put(x + width - cornerRadius); + buffer.put(y + height); + // Upper left corner. + buffer.put(x + cornerRadius); + buffer.put(y + height); + this.addRectangleRoundedCorner(x + cornerRadius, y + height - cornerRadius, cornerRadius, piOver2, piOver2, + cornerSlices, buffer); + buffer.put(x); + buffer.put(y + height - cornerRadius); + // Rewind and return. + buffer.rewind(); + return buffer; + } + + /** + * Creates a vertex buffer for a two-dimensional rectangle at the specified location, with the specified size, and + * with an optional leader pointing to the specified leader location. The rectangle's lower left corner is placed at + * (x, y), and its upper right corner is placed at (x + width, y + height). + *

    + * If the specified leaderWidth is greater than zero and the location (leaderX, leaderY) + * is outside of the rectangle, the rectangle has a triangle attached to one side with with its top pointing at + * (leaderX, leaderY). Otherwise this returns a rectangle with no leader and is equivalent to calling + * {@link #makeRectangle(float, float, float, float)}. The leader is attached at the center of either + * the top, bottom, left, or right side, depending on the leader's location relative to the rectangle. The leader + * width is limited in size by the side it is attached to. For example, if the leader is attached to the rectangle's + * bottom, its width is limited by the rectangle's width. + *

    + * The returned buffer contains pairs of xy coordinates representing the location of each vertex in the rectangle in + * a counter-clockwise winding order relative to the z axis. The buffer may be rendered in OpenGL as either a + * triangle fan or a line loop. + * + * @param x the x-coordinate of the rectangle's lower left corner. + * @param y the y-coordinate of the rectangle's lower left corner. + * @param width the rectangle's width. + * @param height the rectangle's height. + * @param leaderX the x-coordinate the leader points to. + * @param leaderY the y-coordinate the leader points to. + * @param leaderWidth the leader triangle's width. + * + * @return a buffer containing the rectangle's x and y locations. + * + * @throws IllegalArgumentException if any of width, height, or leaderWidth + * are less than zero. + */ + @SuppressWarnings({"SuspiciousNameCombination"}) + public FloatBuffer makeRectangleWithLeader(float x, float y, float width, float height, float leaderX, + float leaderY, float leaderWidth) + { + if (width < 0) + { + String message = Logging.getMessage("Geom.WidthIsNegative"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (height < 0) + { + String message = Logging.getMessage("Geom.HeightIsNegative"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (leaderWidth < 0) + { + String message = Logging.getMessage("Geom.WidthIsNegative"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // Return a rectangle without a leader if the leader width is zero. + if (leaderWidth == 0) + return this.makeRectangle(x, y, width, height); + + int leaderCode = this.computeLeaderLocationCode(x, y, x + width, y + height, leaderX, leaderY); + + // Return a rectangle without a leader if the leader point is inside the rectangle. + if (leaderCode == LEADER_LOCATION_INSIDE) + return this.makeRectangle(x, y, width, height); + + if ((leaderCode & LEADER_LOCATION_BOTTOM) != 0) + { + // Limit the leader's width by the rectangle's width. + if (leaderWidth > width) + leaderWidth = width; + + // The buffer contains seven xy coordinate pairs: two pairs for each corner and three pairs for the leader. + FloatBuffer buffer = BufferUtil.newFloatBuffer(14); + // Start in the leader right corner to ensure the vertices can be drawn as a triangle fan. + buffer.put(x + width / 2f + leaderWidth / 2f); + buffer.put(y); + // Lower right corner. + buffer.put(x + width); + buffer.put(y); + // Upper right corner. + buffer.put(x + width); + buffer.put(y + height); + // Upper left corner. + buffer.put(x); + buffer.put(y + height); + // Lower left corner. + buffer.put(x); + buffer.put(y); + // Leader left corner. + buffer.put(x + width / 2f - leaderWidth / 2f); + buffer.put(y); + // Leader point. + buffer.put(leaderX); + buffer.put(leaderY); + // Rewind and return. + buffer.rewind(); + return buffer; + } + else if ((leaderCode & LEADER_LOCATION_TOP) != 0) + { + // Limit the leader's width by the rectangle's width. + if (leaderWidth > width) + leaderWidth = width; + + // The buffer contains seven xy coordinate pairs: two pairs for each corner and three pairs for the leader. + FloatBuffer buffer = BufferUtil.newFloatBuffer(14); + // Start in the leader left corner to ensure the vertices can be drawn as a triangle fan. + buffer.put(x + width / 2f - leaderWidth / 2f); + buffer.put(y + height); + // Upper left corner. + buffer.put(x); + buffer.put(y + height); + // Lower left corner. + buffer.put(x); + buffer.put(y); + // Lower right corner. + buffer.put(x + width); + buffer.put(y); + // Upper right corner. + buffer.put(x + width); + buffer.put(y + height); + // Leader right corner. + buffer.put(x + width / 2f + leaderWidth / 2f); + buffer.put(y + height); + // Leader point. + buffer.put(leaderX); + buffer.put(leaderY); + // Rewind and return. + buffer.rewind(); + return buffer; + } + else if ((leaderCode & LEADER_LOCATION_LEFT) != 0) + { + // Limit the leader's width by the rectangle's height. + if (leaderWidth > height) + { + //noinspection SuspiciousNameCombination + leaderWidth = height; + } + + // The buffer contains seven xy coordinate pairs: two pairs for each corner and three pairs for the leader. + FloatBuffer buffer = BufferUtil.newFloatBuffer(14); + // Start in the leader bottom corner to ensure the vertices can be drawn as a triangle fan. + buffer.put(x); + buffer.put(y + height / 2f - leaderWidth / 2f); + // Lower left corner. + buffer.put(x); + buffer.put(y); + // Lower right corner. + buffer.put(x + width); + buffer.put(y); + // Upper right corner. + buffer.put(x + width); + buffer.put(y + height); + // Upper left corner. + buffer.put(x); + buffer.put(y + height); + // Leader top corner. + buffer.put(x); + buffer.put(y + height / 2f + leaderWidth / 2f); + // Leader point. + buffer.put(leaderX); + buffer.put(leaderY); + // Rewind and return. + buffer.rewind(); + return buffer; + } + else if ((leaderCode & LEADER_LOCATION_RIGHT) != 0) + { + // Limit the leader's width by the rectangle's height. + if (leaderWidth > height) + { + //noinspection SuspiciousNameCombination + leaderWidth = height; + } + + // The buffer contains seven xy coordinate pairs: two pairs for each corner and three pairs for the leader. + FloatBuffer buffer = BufferUtil.newFloatBuffer(14); + // Start in the leader top corner to ensure the vertices can be drawn as a triangle fan. + buffer.put(x + width); + buffer.put(y + height / 2f + leaderWidth / 2f); + // Upper right corner. + buffer.put(x + width); + buffer.put(y + height); + // Upper left corner. + buffer.put(x); + buffer.put(y + height); + // Lower left corner. + buffer.put(x); + buffer.put(y); + // Lower right corner. + buffer.put(x + width); + buffer.put(y); + // Leader bottom corner. + buffer.put(x + width); + buffer.put(y + height / 2f - leaderWidth / 2f); + // Leader point. + buffer.put(leaderX); + buffer.put(leaderY); + // Rewind and return. + buffer.rewind(); + return buffer; + } + else + { + // Return a rectangle without a leader if the leader location code is unrecognized. This should never + // happen, but we check anyway. + return this.makeRectangle(x, y, width, height); + } + } + + /** + * Creates a vertex buffer for a two-dimensional rectangle at the specified location, with the specified size, and + * with optionally rounded corners. The rectangle's lower left corner is placed at the (x, y), and its + * upper right corner is placed at (x + width, y + height). + *

    + * If the specified cornerRadius and cornerSlices are greater than 0, the rectangle's + * corners have a rounded appearance. The radius specifies the size of a rounded corner, and the slices specifies + * the number of segments that make a rounded corner. If either cornerRadius or + * cornerSlices are 0, this returns a rectangle with sharp corners and is equivalent to calling + * {@link #makeRectangleWithLeader(float, float, float, float, float, float, float)} . The + * cornerRadius is limited by the rectangle's width and height. For example, if the corner radius is + * 100 and the width and height are 50 and 100, the actual corner radius used is 25 - half of the rectangle's + * smallest dimension. + *

    + * If the specified leaderWidth is greater than zero and the location (leaderX, leaderY) + * is outside of the rectangle, the rectangle has a triangle attached to one side with with its top pointing at + * (leaderX, leaderY). Otherwise this returns a rectangle with no leader and is equivalent to calling + * {@link #makeRectangle(float, float, float, float, float, int)}. The leader is attached at the center + * of either the top, bottom, left, or right side, depending on the leader's location relative to the rectangle. The + * leader width is limited in size by the side it is attached to. For example, if the leader is attached to the + * rectangle's bottom, its width is limited by the rectangle's width minus any area used by the rounded corners. + *

    + * The returned buffer contains pairs of xy coordinates representing the location of each vertex in the rectangle in + * a counter-clockwise winding order relative to the z axis. The buffer may be rendered in OpenGL as either a + * triangle fan or a line loop. + * + * @param x the x-coordinate of the rectangle's lower left corner. + * @param y the y-coordinate of the rectangle's lower left corner. + * @param width the rectangle's width. + * @param height the rectangle's height. + * @param cornerRadius the rectangle's rounded corner radius, or 0 to disable rounded corners. + * @param cornerSlices the number of slices in each rounded corner, or 0 to disable rounded corners. + * @param leaderX the x-coordinate the leader points to. + * @param leaderY the y-coordinate the leader points to. + * @param leaderWidth the leader triangle's width. + * + * @return a buffer containing the rectangle's x and y locations. + * + * @throws IllegalArgumentException if any of width, height, cornerRadius, + * cornerSlices, or leaderWidth are less than zero. + */ + public FloatBuffer makeRectangleWithLeader(float x, float y, float width, float height, float cornerRadius, + int cornerSlices, float leaderX, float leaderY, float leaderWidth) + { + if (width < 0) + { + String message = Logging.getMessage("Geom.WidthIsNegative"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (height < 0) + { + String message = Logging.getMessage("Geom.HeightIsNegative"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (cornerRadius < 0) + { + String message = Logging.getMessage("Geom.RadiusIsNegative"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (cornerSlices < 0) + { + String message = Logging.getMessage("generic.NumSlicesIsNegative"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (leaderWidth < 0) + { + String message = Logging.getMessage("Geom.WidthIsNegative"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // Limit the corner radius to half of the rectangles width or height, whichever is smaller. + float maxCornerRadius = Math.min(width, height) / 2f; + if (cornerRadius > maxCornerRadius) + cornerRadius = maxCornerRadius; + + // Create a rectangle with sharp corners if either the corner radius or the number of corner slices is 0. + if (cornerRadius == 0f || cornerSlices == 0) + return this.makeRectangleWithLeader(x, y, width, height, leaderX, leaderY, leaderWidth); + + // Return a rectangle without a leader if the leader width is zero. + if (leaderWidth == 0) + return this.makeRectangle(x, y, width, height, cornerRadius, cornerSlices); + + int leaderCode = this.computeLeaderLocationCode(x, y, x + width, y + height, leaderX, leaderY); + + // Return a rectangle without a leader if the leader point is inside the rectangle. + if (leaderCode == LEADER_LOCATION_INSIDE) + return this.makeRectangle(x, y, width, height, cornerRadius, cornerSlices); + + float piOver2 = (float) Math.PI / 2f; + + if ((leaderCode & LEADER_LOCATION_BOTTOM) != 0) + { + // Limit the leader width by the rectangle's width minus any width used by the rounded corners. + float maxLeaderWidth = width - 2f * cornerRadius; + if (leaderWidth > maxLeaderWidth) + leaderWidth = maxLeaderWidth; + + // The buffer contains two coordinate pairs for each corner, three coordinate pairs for the leader, and two + // coordinate pairs per corner vertex. + FloatBuffer buffer = BufferUtil.newFloatBuffer(22 + 8 * (cornerSlices - 1)); + // Start in the leader right corner to ensure the vertices can be drawn as a triangle fan. + buffer.put(x + width / 2f + leaderWidth / 2f); + buffer.put(y); + // Lower right corner. + buffer.put(x + width - cornerRadius); + buffer.put(y); + this.addRectangleRoundedCorner(x + width - cornerRadius, y + cornerRadius, cornerRadius, -piOver2, piOver2, + cornerSlices, buffer); + buffer.put(x + width); + buffer.put(y + cornerRadius); + // Upper right corner. + buffer.put(x + width); + buffer.put(y + height - cornerRadius); + this.addRectangleRoundedCorner(x + width - cornerRadius, y + height - cornerRadius, cornerRadius, 0f, + piOver2, + cornerSlices, buffer); + buffer.put(x + width - cornerRadius); + buffer.put(y + height); + // Upper left corner. + buffer.put(x + cornerRadius); + buffer.put(y + height); + this.addRectangleRoundedCorner(x + cornerRadius, y + height - cornerRadius, cornerRadius, piOver2, piOver2, + cornerSlices, buffer); + buffer.put(x); + buffer.put(y + height - cornerRadius); + // Lower left corner. + buffer.put(x); + buffer.put(y + cornerRadius); + this.addRectangleRoundedCorner(x + cornerRadius, x + cornerRadius, cornerRadius, (float) Math.PI, piOver2, + cornerSlices, buffer); + buffer.put(x + cornerRadius); + buffer.put(y); + // Leader left corner. + buffer.put(x + width / 2f - leaderWidth / 2f); + buffer.put(y); + // Leader point. + buffer.put(leaderX); + buffer.put(leaderY); + // Rewind and return. + buffer.rewind(); + return buffer; + } + else if ((leaderCode & LEADER_LOCATION_TOP) != 0) + { + // Limit the leader width by the rectangle's width minus any width used by the rounded corners. + float maxLeaderWidth = width - 2f * cornerRadius; + if (leaderWidth > maxLeaderWidth) + leaderWidth = maxLeaderWidth; + + // The buffer contains two coordinate pairs for each corner, three coordinate pairs for the leader, and two + // coordinate pairs per corner vertex. + FloatBuffer buffer = BufferUtil.newFloatBuffer(22 + 8 * (cornerSlices - 1)); + // Start in the leader left corner to ensure the vertices can be drawn as a triangle fan. + buffer.put(x + width / 2f - leaderWidth / 2f); + buffer.put(y + height); + // Upper left corner. + buffer.put(x + cornerRadius); + buffer.put(y + height); + this.addRectangleRoundedCorner(x + cornerRadius, y + height - cornerRadius, cornerRadius, piOver2, piOver2, + cornerSlices, buffer); + buffer.put(x); + buffer.put(y + height - cornerRadius); + // Lower left corner. + buffer.put(x); + buffer.put(y + cornerRadius); + this.addRectangleRoundedCorner(x + cornerRadius, x + cornerRadius, cornerRadius, (float) Math.PI, piOver2, + cornerSlices, buffer); + buffer.put(x + cornerRadius); + buffer.put(y); + // Lower right corner. + buffer.put(x + width - cornerRadius); + buffer.put(y); + this.addRectangleRoundedCorner(x + width - cornerRadius, y + cornerRadius, cornerRadius, -piOver2, piOver2, + cornerSlices, buffer); + buffer.put(x + width); + buffer.put(y + cornerRadius); + // Upper right corner. + buffer.put(x + width); + buffer.put(y + height - cornerRadius); + this.addRectangleRoundedCorner(x + width - cornerRadius, y + height - cornerRadius, cornerRadius, 0f, + piOver2, + cornerSlices, buffer); + buffer.put(x + width - cornerRadius); + buffer.put(y + height); + // Leader right corner. + buffer.put(x + width / 2f + leaderWidth / 2f); + buffer.put(y + height); + // Leader point. + buffer.put(leaderX); + buffer.put(leaderY); + // Rewind and return. + buffer.rewind(); + return buffer; + } + else if ((leaderCode & LEADER_LOCATION_LEFT) != 0) + { + // Limit the leader width by the rectangle's height minus any width used by the rounded corners. + float maxLeaderWidth = height - 2f * cornerRadius; + if (leaderWidth > maxLeaderWidth) + leaderWidth = maxLeaderWidth; + + // The buffer contains two coordinate pairs for each corner, three coordinate pairs for the leader, and two + // coordinate pairs per corner vertex. + FloatBuffer buffer = BufferUtil.newFloatBuffer(22 + 8 * (cornerSlices - 1)); + // Start in the leader bottom corner to ensure the vertices can be drawn as a triangle fan. + buffer.put(x); + buffer.put(y + height / 2f - leaderWidth / 2f); + // Lower left corner. + buffer.put(x); + buffer.put(y + cornerRadius); + this.addRectangleRoundedCorner(x + cornerRadius, x + cornerRadius, cornerRadius, (float) Math.PI, piOver2, + cornerSlices, buffer); + buffer.put(x + cornerRadius); + buffer.put(y); + // Lower right corner. + buffer.put(x + width - cornerRadius); + buffer.put(y); + this.addRectangleRoundedCorner(x + width - cornerRadius, y + cornerRadius, cornerRadius, -piOver2, piOver2, + cornerSlices, buffer); + buffer.put(x + width); + buffer.put(y + cornerRadius); + // Upper right corner. + buffer.put(x + width); + buffer.put(y + height - cornerRadius); + this.addRectangleRoundedCorner(x + width - cornerRadius, y + height - cornerRadius, cornerRadius, 0f, + piOver2, + cornerSlices, buffer); + buffer.put(x + width - cornerRadius); + buffer.put(y + height); + // Upper left corner. + buffer.put(x + cornerRadius); + buffer.put(y + height); + this.addRectangleRoundedCorner(x + cornerRadius, y + height - cornerRadius, cornerRadius, piOver2, piOver2, + cornerSlices, buffer); + buffer.put(x); + buffer.put(y + height - cornerRadius); + // Leader top corner. + buffer.put(x); + buffer.put(y + height / 2f + leaderWidth / 2f); + // Leader point. + buffer.put(leaderX); + buffer.put(leaderY); + // Rewind and return. + buffer.rewind(); + return buffer; + } + else if ((leaderCode & LEADER_LOCATION_RIGHT) != 0) + { + // Limit the leader width by the rectangle's height minus any width used by the rounded corners. + float maxLeaderWidth = height - 2f * cornerRadius; + if (leaderWidth > maxLeaderWidth) + leaderWidth = maxLeaderWidth; + + // The buffer contains two coordinate pairs for each corner, three coordinate pairs for the leader, and two + // coordinate pairs per corner vertex. + FloatBuffer buffer = BufferUtil.newFloatBuffer(22 + 8 * (cornerSlices - 1)); + // Start in the leader top corner to ensure the vertices can be drawn as a triangle fan. + buffer.put(x + width); + buffer.put(y + height / 2f + leaderWidth / 2f); + // Upper right corner. + buffer.put(x + width); + buffer.put(y + height - cornerRadius); + this.addRectangleRoundedCorner(x + width - cornerRadius, y + height - cornerRadius, cornerRadius, 0f, + piOver2, + cornerSlices, buffer); + buffer.put(x + width - cornerRadius); + buffer.put(y + height); + // Upper left corner. + buffer.put(x + cornerRadius); + buffer.put(y + height); + this.addRectangleRoundedCorner(x + cornerRadius, y + height - cornerRadius, cornerRadius, piOver2, piOver2, + cornerSlices, buffer); + buffer.put(x); + buffer.put(y + height - cornerRadius); + // Lower left corner. + buffer.put(x); + buffer.put(y + cornerRadius); + this.addRectangleRoundedCorner(x + cornerRadius, x + cornerRadius, cornerRadius, (float) Math.PI, piOver2, + cornerSlices, buffer); + buffer.put(x + cornerRadius); + buffer.put(y); + // Lower right corner. + buffer.put(x + width - cornerRadius); + buffer.put(y); + this.addRectangleRoundedCorner(x + width - cornerRadius, y + cornerRadius, cornerRadius, -piOver2, piOver2, + cornerSlices, buffer); + buffer.put(x + width); + buffer.put(y + cornerRadius); + // Leader bottom corner. + buffer.put(x + width); + buffer.put(y + height / 2f - leaderWidth / 2f); + // Leader point. + buffer.put(leaderX); + buffer.put(leaderY); + // Rewind and return. + buffer.rewind(); + return buffer; + } + else + { + // Return a rectangle without a leader if the leader location code is unrecognized. This should never + // happen, but we check anyway. + return this.makeRectangle(x, y, width, height, cornerRadius, cornerSlices); + } + } + + /** + * Adds the vertices for one rounded corner of a two-dimensional rectangular to the specified buffer. + * This assumes that the first and last vertices of each corner are created by the caller, so this adds only the + * intermediate vertices. The number of intermediate vertices is equal to slices - 2. This does nothing + * if slices is one or zero. + * + * @param x the x-coordinate of the corner's origin. + * @param y the y-coordinate of the corner's origin. + * @param radius the corner's radius. + * @param start the corner's starting angle, in radians. + * @param sweep the corner's angular distance, in radians. + * @param slices the number of slices in the corner. + * @param buffer the buffer the corner's xy coordinates are added to. + */ + protected void addRectangleRoundedCorner(float x, float y, float radius, float start, float sweep, int slices, + FloatBuffer buffer) + { + if (slices == 0f) + return; + + float step = sweep / (float) slices; + float angle = start + step; + + for (int i = 1; i < slices; i++, angle += step) + { + buffer.put(x + (float) Math.cos(angle) * radius); + buffer.put(y + (float) Math.sin(angle) * radius); + } + } + + /** + * Returns a four bit code indicating the leader's location within the specified rectangle. The rectangle's lower + * left corner is located at (x1, y1) and its upper right corner is located at (x2, y2). + * The returned code includes the bit for any of LEADER_LOCATION_LEFT, + * LEADER_LOCATION_RIGHT, LEADER_LOCATION_BOTTOM, and LEADER_LOCATION_TOP, + * depending on whether the leader is located to the left, right, bottom, or top of the rectangle. If the leader is + * inside the rectangle, this returns LEADER_LOCATION_INSIDE. + * + * @param x1 the rectangle's minimum x-coordinate. + * @param y1 the rectangle's maximum x-coordinate. + * @param x2 the rectangle's minimum y-coordinate. + * @param y2 the rectangle's maximum y-coordinate. + * @param leaderX the leader's x-coordinate. + * @param leaderY the leader's y-coordinate. + * + * @return a four bit code indicating the leader's location relative to the rectangle. + */ + protected int computeLeaderLocationCode(float x1, float y1, float x2, float y2, float leaderX, float leaderY) + { + return (leaderY > y2 ? LEADER_LOCATION_TOP : 0) // bit 0: top + | (leaderY < y1 ? LEADER_LOCATION_BOTTOM : 0) // bit 1: bottom + | (leaderX > x2 ? LEADER_LOCATION_RIGHT : 0) // bit 2: right + | (leaderX < x1 ? LEADER_LOCATION_LEFT : 0); // bit 3: left + } + + //**************************************************************// + //******************** Geometry Support ********************// + //**************************************************************// + + public void reversePoints(int pos, int count, T[] points) + { + if (pos < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "pos=" + pos); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (count < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", "count=" + count); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (points == null) + { + String message = "nullValue.PointsIsNull"; + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (points.length < (pos + count)) + { + String message = Logging.getMessage("generic.ArrayInvalidLength", "points.length < " + (pos + count)); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + T tmp; + int i, j, mid; + + for (i = 0, mid = count >> 1, j = count - 1; i < mid; i++, j--) + { + tmp = points[pos + i]; + points[pos + i] = points[pos + j]; + points[pos + j] = tmp; + } + } + + private int[] copyOf(int[] original, int newLength) + { + int[] copy; + + copy = new int[newLength]; + System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); + + return copy; + } + + private float[] copyOf(float[] original, int newLength) + { + float[] copy; + + copy = new float[newLength]; + System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); + + return copy; + } + + private IntBuffer copyOf(IntBuffer original, int newLength) + { + IntBuffer copy; + + copy = BufferUtil.newIntBuffer(newLength); + original.rewind(); + copy.put(original); + + return copy; + } + + private FloatBuffer copyOf(FloatBuffer original, int newLength) + { + FloatBuffer copy; + + copy = BufferUtil.newFloatBuffer(newLength); + original.rewind(); + copy.put(original); + + return copy; + } + + private void facenorm(float[] srcVerts, int vertA, int vertB, int vertC, float[] dest) + { + int ia, ib, ic; + float[] ab, ac; + + ia = 3 * vertA; + ib = 3 * vertB; + ic = 3 * vertC; + ab = new float[3]; + ac = new float[3]; + + this.sub3(srcVerts, ib, srcVerts, ia, ab, 0); + this.sub3(srcVerts, ic, srcVerts, ia, ac, 0); + this.cross3(ab, ac, dest); + this.norm3AndSet(dest, 0); + } + + private void facenorm(FloatBuffer srcVerts, int vertA, int vertB, int vertC, float[] dest) + { + int ia, ib, ic; + float[] ab, ac; + + ia = 3 * vertA; + ib = 3 * vertB; + ic = 3 * vertC; + ab = new float[3]; + ac = new float[3]; + + this.sub3(srcVerts, ib, srcVerts, ia, ab, 0); + this.sub3(srcVerts, ic, srcVerts, ia, ac, 0); + this.cross3(ab, ac, dest); + this.norm3AndSet(dest, 0); + } + + private void add3AndSet(float[] a, int aPos, float[] b, int bPos) + { + a[aPos] = a[aPos] + b[bPos]; + a[aPos + 1] = a[aPos + 1] + b[bPos + 1]; + a[aPos + 2] = a[aPos + 2] + b[bPos + 2]; + } + + private void add3AndSet(FloatBuffer a, int aPos, float[] b, int bPos) + { + a.put(aPos, a.get(aPos) + b[bPos]); + a.put(aPos + 1, a.get(aPos + 1) + b[bPos + 1]); + a.put(aPos + 2, a.get(aPos + 2) + b[bPos + 2]); + } + + private void sub3(float[] a, int aPos, float[] b, int bPos, float[] dest, int destPos) + { + dest[destPos] = a[aPos] - b[bPos]; + dest[destPos + 1] = a[aPos + 1] - b[bPos + 1]; + dest[destPos + 2] = a[aPos + 2] - b[bPos + 2]; + } + + private void sub3(FloatBuffer a, int aPos, FloatBuffer b, int bPos, float[] dest, int destPos) + { + dest[destPos] = a.get(aPos) - b.get(bPos); + dest[destPos + 1] = a.get(aPos + 1) - b.get(bPos + 1); + dest[destPos + 2] = a.get(aPos + 2) - b.get(bPos + 2); + } + + private void cross3(float[] a, float[] b, float[] dest) + { + dest[0] = a[1] * b[2] - a[2] * b[1]; + dest[1] = a[2] * b[0] - a[0] * b[2]; + dest[2] = a[0] * b[1] - a[1] * b[0]; + } + + private void mul3AndSet(float[] src, int srcPos, float c) + { + src[srcPos] *= c; + src[srcPos + 1] *= c; + src[srcPos + 2] *= c; + } + + private void mul3AndSet(FloatBuffer src, int srcPos, float c) + { + src.put(srcPos, src.get(srcPos) * c); + src.put(srcPos + 1, src.get(srcPos + 1) * c); + src.put(srcPos + 2, src.get(srcPos + 2) * c); + } + + private void mulAndSet(FloatBuffer src, int srcPos, float b, int offset) + { + src.put(srcPos + offset, src.get(srcPos + offset) * b); + } + + private void norm3AndSet(float[] src, int srcPos) + { + float len; + + len = src[srcPos] * src[srcPos] + src[srcPos + 1] * src[srcPos + 1] + src[srcPos + 2] * src[srcPos + 2]; + if (len != 0.0f) + { + len = (float) Math.sqrt(len); + src[srcPos] /= len; + src[srcPos + 1] /= len; + src[srcPos + 2] /= len; + } + } + + private void norm3AndSet(FloatBuffer src, int srcPos) + { + float len; + + len = src.get(srcPos) * src.get(srcPos) + + src.get(srcPos + 1) * src.get(srcPos + 1) + + src.get(srcPos + 2) * src.get(srcPos + 2); + if (len != 0.0f) + { + len = (float) Math.sqrt(len); + src.put(srcPos, src.get(srcPos) / len); + src.put(srcPos + 1, src.get(srcPos + 1) / len); + src.put(srcPos + 2, src.get(srcPos + 2) / len); + } + } + + private int nextPowerOfTwo(int n) + { + int i = 1; + while (i < n) + { + i <<= 1; + } + return i; + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/util/MessageStrings.properties b/WorldWindAndroid/src/gov/nasa/worldwind/util/MessageStrings.properties index 7e293fc..2267a67 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/util/MessageStrings.properties +++ b/WorldWindAndroid/src/gov/nasa/worldwind/util/MessageStrings.properties @@ -34,6 +34,7 @@ term.ImageFormat=Image format term.LevelName=Level name term.LevelNumber=Level number term.NumLevels=Num levels +term.OffGlobe=Off Globe term.Program=Program term.Sector=Sector term.Texture=Texture @@ -53,6 +54,27 @@ generic.ArrayInvalidLength=Array length {0} is invalid generic.BufferInvalidLength=Buffer length {0} is invalid generic.BufferIsEmpty=Buffer is empty generic.CapacityIsInvalid=Capacity {0} is invalid +generic.CannotChangeLayer=Unable to update layer {0} +generic.CannotConvertURLToFile=Unable to convert URL to File path {0} +generic.CannotCopyBufferToSelf=Unable to copy buffer onto itself +generic.CannotCreateColor=Unable to create color {0} +generic.CannotCreateDirectory=Unable to create directory {0} +generic.CannotCreateFile=Unable to create file {0} +generic.CannotCreateLayer=Unable to create layer {0} +generic.CannotCreateObject=Unable to create object {0} +generic.CannotCreateRaster=Unable to create raster {0} +generic.CannotCreateRasterServer=Unable to create raster server instance {0} +generic.CannotCreateTempFile=Unable to create temporary file +generic.CannotDeleteFile=Unable to delete file {0} +generic.CannotMoveFile=Unable to move file {0} to {1} +generic.CannotOpenFile=Unable to open file {0} +generic.CannotOpenInputStream=Unable to open input stream {0} +generic.CannotOpenOutputStream=Unable to open output stream {0} +generic.CannotParse=Unable to parse {0} +generic.CannotParseCapabilities=Unable to parse capabilities {0} +generic.CannotParseInputStream=Unable to read input stream {0} +generic.CannotParseOutputStream=Unable to write output stream {0} +generic.CannotRemoveLayer=Unable to remove layer {0} generic.CheckIntervalIsInvalid=Check interval {0} is invalid generic.ClipDistancesAreInvalid=Clip distances near={0} and far={1} are invalid generic.ColumnIndexOutOfRange=Column index {0} is out of range @@ -174,6 +196,7 @@ nullValue.ListIsNull=List is null nullValue.LocationIsNull=Location is null nullValue.LocationsListIsNull=Locations list is null nullValue.LongitudeIsNull=Longitude is null +nullValue.MaterialIsNull=Material is null nullValue.MatrixIsNull=Matrix is null nullValue.MaxLatitudeIsNull=Min latitude is null nullValue.MaxLongitudeIsNull=Min longitude is null @@ -218,6 +241,7 @@ nullValue.ShapeIsNull=Shape is null nullValue.SourceIsNull=Source is null nullValue.StringIsNull=String is null nullValue.SuffixIsNull=Suffix is null +nullValue.SurfaceTileDrawContextIsNull=Surface tile draw context is null nullValue.TaskIsNull=Task is null nullValue.TerrainIsNull=Terrain is null nullValue.TextureIsNull=Texture is null @@ -237,6 +261,7 @@ nullValue.VectorIsNull=Vector is null nullValue.VertexShaderIsNull=Vertex source is null nullValue.VertexSourceIsNull=Vertex shader source is null or empty nullValue.ViewportIsNull=Viewport is null +nullValue.ViewLimitsIsNull=View limits is null nullValue.WMSCapabilities=WMS capabilities document is null nullValue.WMSLayerNames=WMS layer name list is null or empty nullValue.WMSServiceNameIsNull=Service name is null @@ -273,6 +298,7 @@ BasicRetrievalService.RunningThreadNamePrefix=Running World Wind Retriever:\u002 BasicRetrievalService.UncaughtExceptionDuringRetrieval=Uncaught exception during retrieval on thread {0} BasicSceneController.ExceptionDuringPick=Exception occurred during picking +BasicSceneController.ExceptionDuringPreRendering=Exception encountered while pre-rendering Configuration.AppConfigNotFound=Application configuration {0} not found Configuration.ConversionError=Error parsing configuration value {0} @@ -354,6 +380,17 @@ Matrix.MatrixIsNotSymmetric=Matrix {0} is not symmetric MemoryCache.SizeIsLessThanOne=Size {0} is less than one MemoryCache.SizeIsLargerThanCapacity=Size {0} is larger than cache capacity {1} +OGL.CannotDeleteVBO=Cannot delete VBO because there's no current OpenGL context +OGL.FramebufferComplete=Framebuffer complete +OGL.FramebufferIncompleteAttachment=Framebuffer incomplete: attachment no longer exists, has a dimension of zero, or is a non-renderable buffer +OGL.FramebufferIncompleteDimensions=Framebuffer incomplete: attachments have different dimensions +OGL.FramebufferIncompleteDrawBuffer=Framebuffer incomplete: no draw buffer attachment +OGL.FramebufferIncompleteFormats=Framebuffer incomplete: attachments have different internal formats +OGL.FramebufferIncompleteMissingAttachment=Framebuffer incomplete: no attachments +OGL.FramebufferIncompleteMultisample=Framebuffer incomplete: attachment have different renderbuffer samples +OGL.FramebufferIncompleteReadBuffer=Framebuffer incomplete: no read buffer attachment +OGL.FramebufferUnsupported=Framebuffer unsupported: attachments violate implementation defined restrictions + TaskService.CancellingDuplicateTask=Task service: cancelling duplicate task {0} TaskService.IdleThreadNamePrefix=World Wind Task (Idle) TaskService.RunningThreadNamePrefix=World Wind Task diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/util/NetworkCheckThread.java b/WorldWindAndroid/src/gov/nasa/worldwind/util/NetworkCheckThread.java new file mode 100644 index 0000000..9b49c76 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/util/NetworkCheckThread.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ + +package gov.nasa.worldwind.util; + +import gov.nasa.worldwind.WWObjectImpl; +import gov.nasa.worldwind.WorldWind; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Periodically checks network availability. + * + * @author tag + * @version $Id$ + */ +public class NetworkCheckThread extends Thread +{ + protected static final long DEFAULT_NET_CHECK_INTERVAL = 1000; // milliseconds + + protected WWObjectImpl wwo; + protected AtomicBoolean showNetStatus; + protected AtomicBoolean isNetAvailable; + protected AtomicLong netChecInterval = new AtomicLong(DEFAULT_NET_CHECK_INTERVAL); + + /** + * Constructs a new instance of this class. Once started, the thread checks network availability at a specified + * frequency and stores the result in an atomic variable specified to the constructor. The thread terminates when + * it's interrupted or when a specified boolean atomic variable has the value false. + * + * @param showNetStatus a reference to an atomic variable indicating whether the thread should continue running. + * This variable is tested prior to each network check. The thread terminates when it becomes + * false. + * @param isNetAvailable a reference to an atomic variable in which to write the status of the network check. + * @param interval the interval at which to perform the network check, or null if the default interval of one + * second is to be used. + */ + public NetworkCheckThread(AtomicBoolean showNetStatus, AtomicBoolean isNetAvailable, Long interval) + { + if (showNetStatus == null) + { + String msg = Logging.getMessage("nullValue.StatusReferenceIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + if (isNetAvailable == null) + { + String msg = Logging.getMessage("nullValue.ReturnReferenceIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + this.showNetStatus = showNetStatus; + this.isNetAvailable = isNetAvailable; + + if (interval != null && interval > 0) + this.netChecInterval.set(interval); + } + + @Override + public void run() + { + while (showNetStatus.get() && !Thread.currentThread().isInterrupted()) + { + //noinspection EmptyCatchBlock + try + { + Thread.sleep(DEFAULT_NET_CHECK_INTERVAL); + boolean oldValue = isNetAvailable.getAndSet(!WorldWind.getNetworkStatus().isNetworkUnavailable()); + if(oldValue!=isNetAvailable.get()) + firePropertyChange("isNetAvailable", oldValue, isNetAvailable.get()); + } + catch (InterruptedException e) + { + // Intentionally empty + } + } + } + + public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) { + wwo.addPropertyChangeListener(propertyName, listener); + } + + public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) { + wwo.removePropertyChangeListener(propertyName, listener); + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + wwo.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + wwo.removePropertyChangeListener(listener); + } + + public void firePropertyChange(String propertyName, Object oldValue, Object newValue) { + wwo.firePropertyChange(propertyName, oldValue, newValue); + } + + public void firePropertyChange(PropertyChangeEvent event) { + wwo.firePropertyChange(event); + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/util/OGLRenderToTextureSupport.java b/WorldWindAndroid/src/gov/nasa/worldwind/util/OGLRenderToTextureSupport.java new file mode 100644 index 0000000..ba3cfd0 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/util/OGLRenderToTextureSupport.java @@ -0,0 +1,437 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.util; + +import android.graphics.Rect; +import gov.nasa.worldwind.WorldWindowImpl; +import gov.nasa.worldwind.geom.Matrix; +import gov.nasa.worldwind.render.Color; +import gov.nasa.worldwind.render.DrawContext; +import gov.nasa.worldwind.render.GpuTexture; + +import static android.opengl.GLES20.*; +import static gov.nasa.worldwind.util.OGLStackHandler.*; +import static gov.nasa.worldwind.WorldWindowImpl.glCheckError; + +/** + * OGLRenderToTextureSupport encapsulates the pattern of rendering GL commands to a destination texture. Currently only + * the color pixel values are written to the destination texture, but other values (depth, stencil) should be possible + * with modification or extension. + *

    + * OGLRenderToTextureSupport is compatible with GL version 1.1 or greater, but it attempts to use more recent features + * whenever possible. Different GL feature sets result in different approaches to rendering to texture, therefore the + * caller cannot depend on the mechanism by which OGLRenderToTextureSupport will write pixel values to the destination + * texture. For this reason, OGLRenderToTextureSupport must be used when the contents of the windowing system buffer + * (likely the back framebuffer) can be freely modified by OGLRenderToTextureSupport. The World Wind pre-render stage is + * a good example of when it is appropriate to use OGLRenderToTextureSupport. Fore more information on the pre-render + * stage, see {@link gov.nasa.worldwind.render.PreRenderable} and {@link gov.nasa.worldwind.layers.Layer#preRender(gov.nasa.worldwind.render.DrawContext)}. + * Note: In order to achieve consistent results across all platforms, it is essential to clear the texture's + * contents before rendering anything into the texture. Do this by invoking {@link + * #clear(gov.nasa.worldwind.render.DrawContext, gov.nasa.worldwind.render.Color)} immediately after any call to {@link + * #beginRendering(gov.nasa.worldwind.render.DrawContext, int, int, int, int)}. + *

    + * The common usage pattern for OGLRenderToTextureSupport is as follows:
    DrawContext dc = ...; // Typically + * passed in as an argument to the containing method.
    GpuTexture texture = TextureIO.newTexture(new + * TextureData(...);

    // Setup the drawing rectangle to match the texture dimensions, and originate from the + * texture's lower left corner.
    OGLRenderToTextureSupport rttSupport = new OGLRenderToTextureSupport();
    + * rttSupport.beginRendering(dc, 0, 0, texture.getWidth(), texture.getHeight());
    try
    {
    // Bind the + * texture as the destination for color pixel writes.
    rttSupport.setColorTarget(dc, texture);
    // Clear the + * texture contents with transparent black.
    rttSupport.clear(dc, new Color(0, 0, 0, 0));
    // Invoke desired GL + * rendering commands.
    }
    finally
    {
    rttSupport.endRendering(dc);
    }
    + * + * @author dcollins + * @version $Id: OGLRenderToTextureSupport.java 1676 2013-10-21 18:32:30Z dcollins $ + */ +public class OGLRenderToTextureSupport +{ + protected boolean isFramebufferObjectEnabled; + protected GpuTexture colorTarget; + protected Rect drawRegion = new Rect(); + protected OGLStackHandler stackHandler; + protected int framebufferObject; + protected float[] tmpArrayF = new float[4]; + protected Matrix projection = Matrix.fromIdentity(); + + /** Constructs a new OGLRenderToTextureSupport, but otherwise does nothing. */ + public OGLRenderToTextureSupport() + { + this.isFramebufferObjectEnabled = true; + this.stackHandler = new OGLStackHandler(); + } + + /** + * Returns true if framebuffer objects are enabled for use (only applicable if the feature is available in the + * current GL runtime) + * + * @return true if framebuffer objects are enabled, and false otherwise. + */ + public boolean isEnableFramebufferObject() + { + return this.isFramebufferObjectEnabled; + } + + /** + * Specifies if framebuffer objects should be used if they are available in the current GL runtime. + * + * @param enable true to enable framebuffer objects, false to disable them. + */ + public void setEnableFramebufferObject(boolean enable) + { + if(WorldWindowImpl.DEBUG) + Logging.verbose(String.format("setEnableFramebufferObject(%b)", enable)); + this.isFramebufferObjectEnabled = enable; + } + + /** + * Returns the texture currently set as the color buffer target, or null if no texture is currently bound as the + * color buffer target. + * + * @return the GpuTexture currently set as the color buffer target, or null if none exists. + */ + public GpuTexture getColorTarget() + { + return this.colorTarget; + } + + /** + * Sets the specified texture as the color buffer target. This texture receives the output of all GL commands + * affecting the color buffer. Binding a null texture specifies that no texture should receive color values. If the + * current color target texture is the same reference as the specified texture, this does nothing. Otherwise this + * flushes any buffered pixel values to the current color target, and assigns the specified texture as the new color + * target. + *

    + * If {@link #isEnableFramebufferObject()} is false, the supported texture formats for the color target are limited + * only by the OpenGL implementation's supported formats. If {@link #isEnableFramebufferObject()} is true and the + * DrawContext supports OpenGL framebuffer objects, the supported texture formats for the color target are as + * follows:

    • RGB
    • RGBA
    • FLOAT_R_NV (on NVidia hardware)
    • FLOAT_RG_NV (on NVidia + * hardware)
    • FLOAT_RGB_NV (on NVidia hardware)
    • FLOAT_RGBA_NV (on NVidia hardware)
    + * + * @param dc the current DrawContext. + * @param texture the GpuTexture to use as the destination for GL commands affecting the color buffer. A null value is + * permitted. + * + * @throws IllegalArgumentException if the DrawContext is null. + */ + public void setColorTarget(DrawContext dc, GpuTexture texture) + { + if (dc == null) + { + String message = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (this.colorTarget == texture) + return; + + // If we have a texture target, then write the current GL color buffer state to the current texture target + // before binding a new target. + if (this.colorTarget != null) + { + this.flushColor(dc); + } + + // If framebuffer objects are enabled, then bind the target texture as the current framebuffer's color + // attachment, and GL rendering commands then affect the target texture. Otherwise, GL rendering commands affect + // the windowing system's write buffer (likely the onscreen back buffer), and are explicitly copied to the + // texture in flush() or endRendering(). + if (this.useFramebufferObject(dc)) + { + this.bindFramebufferColorAttachment(dc, texture); + } + + this.colorTarget = texture; + } + + /** + * Clears the current texture target's pixels with the specified RGBA clear color. If the current color texture + * target is null, this does nothing. + * + * @param dc the current DrawContext. + * @param color the RGBA clear color to write to the current color texture target. + * + * @throws IllegalArgumentException if either the DrawContext or the color is null. + */ + public void clear(DrawContext dc, Color color) + { + if (dc == null) + { + String message = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (color == null) + { + String message = Logging.getMessage("nullValue.ColorIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (this.colorTarget == null) + return; + + color.premultiply(); + color.toArray4f(tmpArrayF, 0); + glClearColor(tmpArrayF[0], tmpArrayF[1], tmpArrayF[2], tmpArrayF[3]); + glClear(GL_COLOR_BUFFER_BIT); + } + + /** + * Flushes any buffered pixel values to the appropriate target textures. + * + * @param dc the current DrawContext. + * + * @throws IllegalArgumentException if the DrawContext is null. + */ + public void flush(DrawContext dc) + { + if (dc == null) + { + String message = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.flushColor(dc); + } + + /** + * Configures the GL attached to the specified DrawContext for rendering a 2D model to a texture. The modelview + * matrix is set to the identity, the projection matrix is set to an orthographic projection aligned with the + * specified draw rectangle (x, y, width, height), the viewport and scissor boxes are set to the specified draw + * rectangle, and the depth test and depth write flags are disabled. Because the viewport and scissor boxes are set + * to the draw rectangle, only the texels intersecting the specified drawing rectangle (x, y, width, height) are + * affected by GL commands. Once rendering is complete, this should always be followed with a call to {@link + * #endRendering(gov.nasa.worldwind.render.DrawContext)}. + * + * @param dc the current DrawContext. + * @param x the x-coordinate of the draw region's lower left corner. + * @param y the y-coordinate of the draw region's lower left corner. + * @param width the draw region width. + * @param height the draw region height. + * @return Orthographic projection matrix + * @throws IllegalArgumentException if the DrawContext is null. + */ + public Matrix beginRendering(DrawContext dc, int x, int y, int width, int height) + { + if (dc == null) + { + String message = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + this.drawRegion.set(x, y, width, height); + + projection.setOrthographic(x, x+width, y, y+height, -1, 1); + + // Note: there is no attribute bit for framebuffer objects. The default framebuffer object state (object ID 0 + // is bound as the current fbo) is restored in endRendering(). + this.stackHandler.pushAttrib( + GL_COLOR_BUFFER_BIT // For clear color. + | GL_DEPTH_BUFFER_BIT // For depth test and depth mask. + | GL_SCISSOR_BIT // For scissor test and scissor box. + | GL_VIEWPORT_BIT + ); // For viewport state. + + // Disable the depth test and writing to the depth buffer. This provides consistent render to texture behavior + // regardless of whether we are using copy-to-texture or framebuffer objects. For copy-to-texture, the depth + // test and depth writing are explicitly disabled. For fbos there is no depth buffer components, so the depth + // dest is implicitly disabled. + glDisable(GL_DEPTH_TEST); + glDepthMask(false); + // Enable the scissor test and set both the scissor box and the viewport to the specified region. This enables + // the caller to set up rendering to a subset of the texture. Note that the scissor box defines the region + // affected by a call to glClear(). + glEnable(GL_SCISSOR_TEST); + glScissor(x, y, width, height); + glViewport(x, y, width, height); + + if (this.useFramebufferObject(dc)) + { + this.beginFramebufferObjectRendering(dc); + } + return projection; + } + + /** + * Flushes any buffered pixel values to the appropriate texure targets, then restores the GL state to its previous + * configuration before {@link #beginRendering(gov.nasa.worldwind.render.DrawContext, int, int, int, int)} was + * called. Finally, all texture targets associated with this OGLRenderToTextureSupport are unbound. + * + * @param dc the current DrawContext. + * + * @throws IllegalArgumentException if the DrawContext is null. + */ + public void endRendering(DrawContext dc) + { + if (dc == null) + { + String message = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.flush(dc); + + if (this.useFramebufferObject(dc)) + { + if (this.colorTarget != null) + { + this.bindFramebufferColorAttachment(dc, null); + } + + this.endFramebufferObjectRendering(dc); + } + + this.stackHandler.popAttrib(); + this.colorTarget = null; + } + + protected void flushColor(DrawContext dc) + { + // If framebuffer objects are enabled, then texture contents are already affected by the any GL rendering + // commands. + if (this.useFramebufferObject(dc)) + { + if (this.colorTarget != null) + { + this.colorTarget.bind(); + glGenerateMipmap(this.colorTarget.getTarget()); + } + } + // If framebuffer objects are not enabled, then we've been rendering into the read buffer associated with the + // windowing system (likely the onscreen back buffer). Explicitly copy the read buffer contents to the texture. + else + { + if (this.colorTarget != null) + { + this.copyScreenPixelsToTexture(dc, this.drawRegion.left, this.drawRegion.top, + this.drawRegion.width(), this.drawRegion.height(), this.colorTarget); + } + } + } + + protected void copyScreenPixelsToTexture(DrawContext dc, int x, int y, int width, int height, GpuTexture texture) + { + int w = width; + int h = height; + + // If the lower left corner of the region to copy is outside of the texture bounds, then exit and do nothing. + if (x >= texture.getWidth() || y >= texture.getHeight()) + return; + + // Limit the dimensions of the region to copy so they fit into the texture's dimensions. + if (w > texture.getWidth()) + w = texture.getWidth(); + if (h > texture.getHeight()) + h = texture.getHeight(); + + // We want to copy the contents of the current GL read buffer to the specified texture target. However we do + // not want to change any of the texture creation parameters (e.g. dimensions, internal format, border). + // Therefore we use glCopyTexSubImage2D() to copy a region of the read buffer to a region of the texture. + // glCopyTexSubImage2D() has two key features: + // 1. Does not change the texture creation parameters. We want to upload a new region of data without + // changing the textures' defining parameters. + // 2. Enables specification of a destination (x, y) offset in texels. This offset corresponds to the + // viewport (x, y) specified by the caller in beginRendering(). + texture.bind(); + glCopyTexSubImage2D( + texture.getTarget(), // target + 0, // level + x, y, // xoffset, yoffset + x, y, w, h); // x, y, width, height + glCheckError("glCopyTexSubImage2D"); + } + + protected boolean useFramebufferObject(DrawContext dc) + { + return isEnableFramebufferObject() && GLRuntimeCapabilities.getInstance().isUseFramebufferObject(); + } + + protected void beginFramebufferObjectRendering(DrawContext dc) + { + if(WorldWindowImpl.DEBUG) + Logging.verbose("binding FrameBuffer Object"); + // Binding a framebuffer object causes all GL operations to operate on the attached textures and renderbuffers + // (if any). + + int[] framebuffers = new int[1]; + + glGenFramebuffers(1, framebuffers, 0); + glBindFramebuffer(GL_FRAMEBUFFER, framebuffers[0]); + + this.framebufferObject = framebuffers[0]; + if (this.framebufferObject == 0) + { + throw new IllegalStateException("Frame Buffer Object not created."); + } + } + + protected void endFramebufferObjectRendering(DrawContext dc) + { + // Binding framebuffer object 0 (the default) causes GL operations to operate on the window system attached + // framebuffer. + + int[] framebuffers = new int[] {this.framebufferObject}; + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, framebuffers, 0); + + this.framebufferObject = 0; + } + + protected void bindFramebufferColorAttachment(DrawContext dc, GpuTexture texture) + { + // Attach the texture as color attachment 0 to the framebuffer. + if (texture != null) + { + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + texture.getTextureId(), 0); + this.checkFramebufferStatus(dc); + } + // If the texture is null, detach color attachment 0 from the framebuffer. + else + { + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + } + } + + protected void checkFramebufferStatus(DrawContext dc) + { + int status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + + switch (status) + { + // Framebuffer is configured correctly and supported on this hardware. + case GL_FRAMEBUFFER_COMPLETE: + break; + // Framebuffer is configured correctly, but not supported on this hardware. + case GL_FRAMEBUFFER_UNSUPPORTED: + throw new IllegalStateException(getFramebufferStatusString(status)); + // Framebuffer is configured incorrectly. This should never happen, but we check anyway. + default: + throw new IllegalStateException(getFramebufferStatusString(status)); + } + } + + protected static String getFramebufferStatusString(int status) + { + switch (status) + { + case GL_FRAMEBUFFER_COMPLETE: + return Logging.getMessage("OGL.FramebufferComplete"); + case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: + return Logging.getMessage("OGL.FramebufferIncompleteAttachment"); + case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS: + return Logging.getMessage("OGL.FramebufferIncompleteDimensions"); + case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: + return Logging.getMessage("OGL.FramebufferIncompleteMissingAttachment"); + default: + return null; + } + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/util/OGLStackHandler.java b/WorldWindAndroid/src/gov/nasa/worldwind/util/OGLStackHandler.java new file mode 100644 index 0000000..2c11aa9 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/util/OGLStackHandler.java @@ -0,0 +1,216 @@ +package gov.nasa.worldwind.util; + +import java.util.Stack; + +import static android.opengl.GLES20.*; + +/** + * Handles openGL state stack + * Created by kedzie on 5/9/14. + */ +public class OGLStackHandler { + + public static int GL_POLYGON_BIT=8; + public static int GL_VIEWPORT_BIT=2048; + public static int GL_SCISSOR_BIT=524288; + + private static class DepthState { + boolean depthTestEnabled; + int []iState = new int[2]; + float []depthClear = new float[1]; + + DepthState save() { + depthTestEnabled = glIsEnabled(GL_DEPTH_TEST); + glGetIntegerv(GL_DEPTH_WRITEMASK, iState, 0); + glGetIntegerv(GL_DEPTH_FUNC, iState, 1); + glGetFloatv(GL_DEPTH_CLEAR_VALUE, depthClear, 0); + return this; + } + + void restore() { + if(depthTestEnabled) + glEnable(GL_DEPTH_TEST); + else + glDisable(GL_DEPTH_TEST); + glDepthMask(iState[0]>0); + glDepthFunc(iState[1]); + glClearDepthf(depthClear[0]); + } + } + + private static class ColorState { + boolean blendEnabled; + int []iState = new int[6]; + float []fState = new float[4]; + + ColorState save() { + blendEnabled = glIsEnabled(GL_BLEND); + glGetIntegerv(GL_BLEND_SRC_RGB, iState, 0); + glGetIntegerv(GL_BLEND_DST_RGB, iState, 1); + glGetIntegerv(GL_BLEND_SRC_ALPHA, iState, 2); + glGetIntegerv(GL_BLEND_DST_ALPHA, iState, 3); + glGetIntegerv(GL_BLEND_EQUATION_RGB, iState, 4); + glGetIntegerv(GL_BLEND_EQUATION_ALPHA, iState, 5); + glGetFloatv(GL_BLEND_COLOR, fState, 0); + return this; + } + + void restore() { + if(blendEnabled) + glEnable(GL_BLEND); + else + glDisable(GL_BLEND); + glBlendFuncSeparate(iState[0], iState[1], iState[2],iState[3]); + glBlendEquationSeparate(iState[4], iState[5]); + glBlendColor(fState[0], fState[1], fState[2], fState[3]); + } + } + + private static class PolygonState { + boolean cullEnabled; + boolean polygonOffsetEnabled; + int []iState = new int[4]; + + PolygonState save() { + cullEnabled = glIsEnabled(GL_CULL_FACE); + polygonOffsetEnabled = glIsEnabled(GL_POLYGON_OFFSET_FILL); + glGetIntegerv(GL_CULL_FACE_MODE, iState, 0); + glGetIntegerv(GL_FRONT_FACE, iState, 1); + glGetIntegerv(GL_POLYGON_OFFSET_FACTOR, iState, 2); + glGetIntegerv(GL_POLYGON_OFFSET_UNITS, iState, 3); + return this; + } + + void restore() { + if(cullEnabled) + glEnable(GL_CULL_FACE); + else + glDisable(GL_CULL_FACE); + glCullFace(iState[0]); + glFrontFace(iState[1]); + if(polygonOffsetEnabled) + glEnable(GL_POLYGON_OFFSET_FILL); + else + glDisable(GL_POLYGON_OFFSET_FILL); + glPolygonOffset(iState[2], iState[3]); + } + } + + private static class ScissorState { + boolean enabled; + int[] box = new int[4]; + + ScissorState save() { + enabled = glIsEnabled(GL_SCISSOR_TEST); + glGetIntegerv(GL_SCISSOR_BOX, box, 0); + return this; + } + + void restore() { + if(enabled) + glEnable(GL_SCISSOR_TEST); + else + glDisable(GL_SCISSOR_TEST); + glScissor(box[0], box[1], box[2], box[3]); + } + } + + private static class StencilState { + boolean enabled; + int[] iState = new int[14]; + + StencilState save() { + enabled = glIsEnabled(GL_STENCIL_TEST); + glGetIntegerv(GL_STENCIL_FUNC, iState, 0); + glGetIntegerv(GL_STENCIL_VALUE_MASK, iState, 1); + glGetIntegerv(GL_STENCIL_REF, iState, 2); + glGetIntegerv(GL_STENCIL_BACK_FUNC, iState, 3); + glGetIntegerv(GL_STENCIL_BACK_VALUE_MASK, iState, 4); + glGetIntegerv(GL_STENCIL_BACK_REF, iState, 5); + glGetIntegerv(GL_STENCIL_FAIL, iState, 6); + glGetIntegerv(GL_STENCIL_PASS_DEPTH_FAIL, iState, 7); + glGetIntegerv(GL_STENCIL_PASS_DEPTH_PASS, iState, 8); + glGetIntegerv(GL_STENCIL_BACK_FAIL, iState, 9); + glGetIntegerv(GL_STENCIL_BACK_PASS_DEPTH_FAIL, iState, 10); + glGetIntegerv(GL_STENCIL_BACK_PASS_DEPTH_PASS, iState, 11); + glGetIntegerv(GL_STENCIL_WRITEMASK, iState, 12); + glGetIntegerv(GL_STENCIL_BACK_WRITEMASK, iState, 13); + return this; + } + + void restore() { + if(enabled) + glEnable(GL_STENCIL_TEST); + else + glDisable(GL_STENCIL_TEST); + glStencilFuncSeparate(GL_FRONT, iState[0], iState[1], iState[2]); + glStencilFuncSeparate(GL_BACK, iState[3], iState[4], iState[5]); + glStencilOpSeparate(GL_FRONT, iState[6], iState[7], iState[8]); + glStencilOpSeparate(GL_BACK, iState[9], iState[10], iState[11]); + glStencilMaskSeparate(GL_FRONT, iState[12]); + glStencilMaskSeparate(GL_FRONT, iState[13]); + } + } + + private static class ViewportState { + int[] viewport = new int[4]; + float[] depthRange = new float[2]; + + ViewportState save() { + glGetIntegerv(GL_VIEWPORT, viewport, 0); + glGetFloatv(GL_DEPTH_RANGE, depthRange, 0); + return this; + } + + void restore() { + glViewport(viewport[0], viewport[1], viewport[2], viewport[3]); + glDepthRangef(depthRange[0], depthRange[1]); + } + } + + private Stack depthStateStack = new Stack(); + private Stack colorStateStack = new Stack(); + private Stack polygonStateStack = new Stack(); + private Stack scissorStateStack = new Stack(); + private Stack stencilStateStack = new Stack(); + private Stack viewportStateStack = new Stack(); + + private int lastPushedBits; + + public void pushAttrib(int bits) { + this.lastPushedBits = bits; + if((bits & GL_DEPTH_BUFFER_BIT) == GL_DEPTH_BUFFER_BIT) + depthStateStack.push(new DepthState().save()); + if((bits & GL_COLOR_BUFFER_BIT) == GL_COLOR_BUFFER_BIT) + colorStateStack.push(new ColorState().save()); + if((bits & GL_POLYGON_BIT) == GL_POLYGON_BIT) + polygonStateStack.push(new PolygonState().save()); + if((bits & GL_STENCIL_BUFFER_BIT) == GL_STENCIL_BUFFER_BIT) + stencilStateStack.push(new StencilState().save()); + if((bits & GL_SCISSOR_BIT) == GL_SCISSOR_BIT) + scissorStateStack.push(new ScissorState().save()); + if((bits & GL_VIEWPORT_BIT) == GL_VIEWPORT_BIT) + viewportStateStack.push(new ViewportState().save()); + } + + public void popAttrib() { + popAttrib(lastPushedBits); + } + + public void popAttrib(int bits) { + lastPushedBits = 0; + if((bits & GL_DEPTH_BUFFER_BIT) == GL_DEPTH_BUFFER_BIT) + depthStateStack.pop().restore(); + if((bits & GL_COLOR_BUFFER_BIT) == GL_COLOR_BUFFER_BIT) + colorStateStack.pop().restore(); + if((bits & GL_POLYGON_BIT) == GL_POLYGON_BIT) + polygonStateStack.pop().restore(); + if((bits & GL_STENCIL_BUFFER_BIT) == GL_STENCIL_BUFFER_BIT) + stencilStateStack.pop().restore(); + if((bits & GL_SCISSOR_BIT) == GL_SCISSOR_BIT) + scissorStateStack.pop().restore(); + if((bits & GL_VIEWPORT_BIT) == GL_VIEWPORT_BIT) + viewportStateStack.pop().restore(); + } + +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/util/OGLUtil.java b/WorldWindAndroid/src/gov/nasa/worldwind/util/OGLUtil.java new file mode 100644 index 0000000..930dfce --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/util/OGLUtil.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ + +package gov.nasa.worldwind.util; + +import android.graphics.Bitmap; +import android.opengl.GLES20; +import android.opengl.GLUtils; + +import static gov.nasa.worldwind.WorldWindowImpl.glCheckError; + +import static android.opengl.GLES20.*; + +/** + * Created by kedzie on 5/9/14. + */ +public class OGLUtil { + + /** + * Sets the GL blending state according to the specified color mode. If havePremultipliedColors is + * true, this applies a blending function appropriate for colors premultiplied by the alpha component. Otherwise, + * this applies a blending function appropriate for non-premultiplied colors. + * + * @param havePremultipliedColors true to configure blending for colors premultiplied by the alpha components, and + * false to configure blending for non-premultiplied colors. + * + * @throws IllegalArgumentException if the GL is null. + */ + public static void applyBlending(boolean havePremultipliedColors) + { + if (havePremultipliedColors) + { + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + glCheckError("glBlendFunc"); + } + else + { + // The separate blend function correctly handles regular (non-premultiplied) colors. We want + // Cd = Cs*As + Cf*(1-As) + // Ad = As + Af*(1-As) + // So we use GL_EXT_blend_func_separate to specify different blending factors for source color and source + // alpha. +// String extensions = glGetString(GL_EXTENSIONS); + +// boolean haveExtBlendFuncSeparate = isExtensionAvailable(GL_EXT_BLEND_FUNC_SEPARATE); +// if (haveExtBlendFuncSeparate) +// { + glBlendFuncSeparate( + GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, // rgb blending factors + GL_ONE, GL_ONE_MINUS_SRC_ALPHA); // alpha blending factors + glCheckError("glBlendFuncSeparate"); +// } +// else +// { +// glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); +// } + } + } + + /** + * @param internalFormat the OpenGL texture internal format. + * @param width the texture width, in pixels. + * @param height the texture height, in pixels. + * @param includeMipmaps true to include the texture's mip map data in the estimated size; false otherwise. + * + * @return a pixel format corresponding to the texture internal format, or 0 if the internal format is not + * recognized. + * + * @throws IllegalArgumentException if either the width or height is less than or equal to zero. + */ + public static long estimateMemorySize(int internalFormat, int pixelType, int width, int height, boolean includeMipmaps) + { + if (width < 0) + { + String message = Logging.getMessage("Geom.WidthInvalid", width); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (height < 0) + { + String message = Logging.getMessage("Geom.HeightInvalid", height); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + int numPixels = width * height; + + // Add the number of pixels from each level in the mipmap chain to the total number of pixels. + if (includeMipmaps) + { + int maxLevel = Math.max((int) WWMath.logBase2(width), (int) WWMath.logBase2(height)); + for (int level = 1; level <= maxLevel; level++) + { + int w = Math.max(width >> level, 1); + int h = Math.max(height >> level, 1); + numPixels += w * h; + } + } + + switch(internalFormat) { + case GL_ALPHA: + case GL_LUMINANCE: + // Alpha and luminance pixel data is always stored as 1 byte per pixel. See OpenGL ES Specification, version 2.0.25, + // section 3.6.2, table 3.4. + return numPixels; + case GL_LUMINANCE_ALPHA: + // Luminance-alpha pixel data is always stored as 2 bytes per pixel. See OpenGL ES Specification, + // version 2.0.25, section 3.6.2, table 3.4. + return 2 * numPixels; // Type must be GL_UNSIGNED_BYTE. + case GL_RGB: + // RGB pixel data is stored as either 2 or 3 bytes per pixel, depending on the type used during texture + // image specification. See OpenGL ES Specification, version 2.0.25, section 3.6.2, table 3.4. + // Default to type GL_UNSIGNED_BYTE. + return (pixelType == GLES20.GL_UNSIGNED_SHORT_5_6_5 ? 2 : 3) * numPixels; + default: // Default to internal format GL_RGBA. + // RGBA pixel data is stored as either 2 or 4 bytes per pixel, depending on the type used during texture + // image specification. See OpenGL ES Specification, version 2.0.25, section 3.6.2, table 3.4. + // Default to type GL_UNSIGNED_BYTE. + return ((pixelType == GLES20.GL_UNSIGNED_SHORT_4_4_4_4 || pixelType == GLES20.GL_UNSIGNED_SHORT_5_5_5_1) ? 2 : 4) * numPixels; + } + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/util/PerformanceStatistic.java b/WorldWindAndroid/src/gov/nasa/worldwind/util/PerformanceStatistic.java new file mode 100644 index 0000000..bb61cf0 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/util/PerformanceStatistic.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.util; + +import java.util.HashSet; +import java.util.Set; + +public class PerformanceStatistic implements Comparable +{ + public static final String ALL = "gov.nasa.worldwind.perfstat.All"; + public static final String AIRSPACE_GEOMETRY_COUNT = "gov.nasa.worldwind.perfstat.AirspaceGeometryCount"; + public static final String AIRSPACE_VERTEX_COUNT = "gov.nasa.worldwind.perfstat.AirspaceVertexCount"; + public static final String FRAME_RATE = "gov.nasa.worldwind.perfstat.FrameRate"; + public static final String FRAME_TIME = "gov.nasa.worldwind.perfstat.FrameTime"; + public static final String IMAGE_TILE_COUNT = "gov.nasa.worldwind.perfstat.ImageTileCount"; + public static final String TERRAIN_TILE_COUNT = "gov.nasa.worldwind.perfstat.TerrainTileCount"; + public static final String MEMORY_CACHE = "gov.nasa.worldwind.perfstat.MemoryCache"; + public static final String PICK_TIME = "gov.nasa.worldwind.perfstat.PickTime"; + public static final String JVM_HEAP = "gov.nasa.worldwind.perfstat.JvmHeap"; + public static final String JVM_HEAP_USED = "gov.nasa.worldwind.perfstat.JvmHeapUsed"; + public static final String TEXTURE_CACHE = "gov.nasa.worldwind.perfstat.TextureCache"; + + public static final Set ALL_STATISTICS_SET = new HashSet(1); + static + { + ALL_STATISTICS_SET.add(PerformanceStatistic.ALL); + } + + private final String key; + private final String displayString; + private final Object value; + + public PerformanceStatistic(String key, String displayString, Object value) + { + this.key = key; + this.displayString = displayString; + this.value = value; + } + + public String getKey() + { + return key; + } + + public String getDisplayString() + { + return displayString; + } + + public Object getValue() + { + return value; + } + + public int compareTo(PerformanceStatistic that) + { + //noinspection StringEquality + if (this.displayString == that.displayString) + return 0; + + if (this.displayString != null && that.displayString != null) + return this.displayString.compareTo(that.displayString); + + return this.displayString == null ? -1 : 1; + } + + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + PerformanceStatistic that = (PerformanceStatistic) o; + + if (displayString != null ? !displayString.equals(that.displayString) : that.displayString != null) + return false; + if (key != null ? !key.equals(that.key) : that.key != null) + return false; + //noinspection RedundantIfStatement + if (value != null ? !value.equals(that.value) : that.value != null) + return false; + + return true; + } + + public int hashCode() + { + int result; + result = (key != null ? key.hashCode() : 0); + result = 31 * result + (displayString != null ? displayString.hashCode() : 0); + result = 31 * result + (value != null ? value.hashCode() : 0); + return result; + } + + public String toString() + { + return this.displayString + " " + this.value.toString(); + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/util/PickPointFrustumList.java b/WorldWindAndroid/src/gov/nasa/worldwind/util/PickPointFrustumList.java new file mode 100644 index 0000000..d2acdfb --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/util/PickPointFrustumList.java @@ -0,0 +1,311 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.util; + +import android.graphics.Point; +import android.graphics.Rect; +import gov.nasa.worldwind.geom.Extent; +import gov.nasa.worldwind.geom.PickPointFrustum; +import gov.nasa.worldwind.geom.Vec4; + +import java.util.ArrayList; + +/** + * @author Jeff Addison + * @version $Id$ + */ +public class PickPointFrustumList extends ArrayList +{ + public PickPointFrustumList() + { + } + + public PickPointFrustumList(PickPointFrustumList list) + { + super(list); + } + + /** + * Returns true if the specified point is inside the space enclosed by ALL of the frustums + * + * @param point the point to test. + * + * @return true if the specified point is inside the space enclosed by ALL the Frustums, and false otherwise. + * + * @throws IllegalArgumentException if the point is null. + */ + public final boolean containsInAll(Vec4 point) + { + if (point == null) + { + String msg = Logging.getMessage("nullValue.PointIsNull"); + Logging.verbose(msg); + throw new IllegalArgumentException(msg); + } + + for (PickPointFrustum frustum : this) + { + if (!frustum.contains(point)) + { + return false; + } + } + + return true; + } + + /** + * Returns true if the specified point is inside the space enclosed by ANY of the frustums + * + * @param point the point to test. + * + * @return true if the specified point is inside the space enclosed by ANY the Frustums, and false otherwise. + * + * @throws IllegalArgumentException if the point is null. + */ + public final boolean containsInAny(Vec4 point) + { + if (point == null) + { + String msg = Logging.getMessage("nullValue.PointIsNull"); + Logging.verbose(msg); + throw new IllegalArgumentException(msg); + } + + for (PickPointFrustum frustum : this) + { + if (frustum.contains(point)) + { + return true; + } + } + + return false; + } + + /** + * Returns true if the specified 2D screen point is inside the 2D screen rectangle enclosed by ALL of the frustums + * + * @param point the point to test. + * + * @return true if the specified point is inside the space enclosed by ALL the Frustums, and false otherwise. + * + * @throws IllegalArgumentException if the point is null. + */ + public final boolean containsInAll(Point point) + { + if (point == null) + { + String msg = Logging.getMessage("nullValue.PointIsNull"); + Logging.verbose(msg); + throw new IllegalArgumentException(msg); + } + + for (PickPointFrustum frustum : this) + { + if (!frustum.contains(point.x, point.y)) + { + return false; + } + } + + return true; + } + + /** + * Returns true if the specified 2D point is inside the 2D screen rectangle enclosed by ANY of the frustums + * + * @param x the x coordinate to test. + * @param y the y coordinate to test. + * + * @return true if the specified point is inside the space enclosed by ANY the Frustums, and false otherwise. + * + * @throws IllegalArgumentException if the point is null. + */ + public final boolean containsInAny(double x, double y) + { + for (PickPointFrustum frustum : this) + { + if (frustum.contains(x, y)) + { + return true; + } + } + + return false; + } + + /** + * Returns true if the specified 2D point is inside the 2D screen rectangle enclosed by ANY of the frustums + * + * @param point the point to test. + * + * @return true if the specified point is inside the space enclosed by ANY the Frustums, and false otherwise. + * + * @throws IllegalArgumentException if the point is null. + */ + public final boolean containsInAny(Point point) + { + if (point == null) + { + String msg = Logging.getMessage("nullValue.PointIsNull"); + Logging.verbose(msg); + throw new IllegalArgumentException(msg); + } + + for (PickPointFrustum frustum : this) + { + if (frustum.contains(point.x, point.y)) + { + return true; + } + } + + return false; + } + + /** + * Returns true if the specified {@link gov.nasa.worldwind.geom.Extent} intersects the space enclosed by ALL the Frustums. NOTE: Cannot be + * true if all frustums do not intersect. + * + * @param extent the Extent to test. + * + * @return true if the specified Extent intersects the space enclosed by ALL Frustums, and false otherwise. + * + * @throws IllegalArgumentException if the extent is null. + */ + public final boolean intersectsAll(Extent extent) + { + if (extent == null) + { + String msg = Logging.getMessage("nullValue.ExtentIsNull"); + Logging.verbose(msg); + throw new IllegalArgumentException(msg); + } + + for (PickPointFrustum frustum : this) + { + if (!frustum.intersects(extent)) + { + return false; + } + } + + return true; + } + + /** + * Returns true if the specified {@link gov.nasa.worldwind.geom.Extent} intersects the space enclosed by ANY of the Frustums. + * + * @param extent the Extent to test. + * + * @return true if the specified Extent intersects the space enclosed by ANY of the Frustums, and false otherwise. + * + * @throws IllegalArgumentException if the extent is null. + */ + public final boolean intersectsAny(Extent extent) + { + if (extent == null) + { + String msg = Logging.getMessage("nullValue.ExtentIsNull"); + Logging.verbose(msg); + throw new IllegalArgumentException(msg); + } + + for (PickPointFrustum frustum : this) + { + if (frustum.intersects(extent)) + { + return true; + } + } + + return false; + } + + /** + * Returns true if a specified line segment intersects the space enclosed by ANY of the Frustums. + * + * @param pa one end of the segment. + * @param pb the other end of the segment. + * + * @return true if the specified segment intersects the space enclosed by ANY of the Frustums, otherwise false. + * + * @throws IllegalArgumentException if either point is null. + */ + public final boolean intersectsAny(Vec4 pa, Vec4 pb) + { + for (PickPointFrustum frustum : this) + { + if (frustum.intersectsSegment(pa, pb)) + return true; + } + + return false; + } + + /** + * Returns true if the specified {@link android.graphics,Rect} intersects the 2D screen space enclosed by ALL the + * Frustums. NOTE: Cannot be true if all frustums do not intersect. + * + * @param rect the Rectangle to test. + * + * @return true if the specified Rectangle intersects the 2D screen space enclosed by ALL Frustums, and false + * otherwise. + * + * @throws IllegalArgumentException if the extent is null. + */ + public final boolean intersectsAll(Rect rect) + { + if (rect == null) + { + String msg = Logging.getMessage("nullValue.RectangleIsNull"); + Logging.verbose(msg); + throw new IllegalArgumentException(msg); + } + + for (PickPointFrustum frustum : this) + { + if (!frustum.intersects(rect)) + { + return false; + } + } + + return true; + } + + /** + * Returns true if the specified {@link android.graphics.Rect} intersects the 2D screen space enclosed by ANY of the + * Frustums. + * + * @param rect the Rectangle to test. + * + * @return true if the specified Rectangle intersects the 2D screen space enclosed by ANY of the Frustums, and false + * otherwise. + * + * @throws IllegalArgumentException if the extent is null. + */ + public final boolean intersectsAny(Rect rect) + { + if (rect == null) + { + String msg = Logging.getMessage("nullValue.RectangleIsNull"); + Logging.verbose(msg); + throw new IllegalArgumentException(msg); + } + + for (PickPointFrustum frustum : this) + { + if (frustum.intersects(rect)) + { + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/util/RayCastingSupport.java b/WorldWindAndroid/src/gov/nasa/worldwind/util/RayCastingSupport.java new file mode 100644 index 0000000..ed5989d --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/util/RayCastingSupport.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.util; + +import gov.nasa.worldwind.geom.Intersection; +import gov.nasa.worldwind.geom.Line; +import gov.nasa.worldwind.geom.Position; +import gov.nasa.worldwind.geom.Vec4; +import gov.nasa.worldwind.globes.Globe; + +/** + * Contains methods to resolve ray intersections with the terrain. + * @author Patrick Murris + * @version $Id$ + */ + +public class RayCastingSupport +{ + private static double defaultSampleLength = 100; // meters + private static double defaultPrecision = 10; // meters + + /** + * Compute the intersection Position of the globe terrain with the ray starting + * at origin in the given direction. Uses default sample length and result precision. + * @param globe the globe to intersect with. + * @param origin origin of the ray. + * @param direction direction of the ray. + * @return the Position found or null. + */ + public static Position intersectRayWithTerrain(Globe globe, Vec4 origin, Vec4 direction, Position out) + { + return intersectRayWithTerrain(globe, origin, direction, defaultSampleLength, defaultPrecision, out); + } + + /** + * Compute the intersection Position of the globe terrain with the ray starting + * at origin in the given direction. Uses the given sample length and result precision. + * @param globe the globe to intersect with. + * @param origin origin of the ray. + * @param direction direction of the ray. + * @param sampleLength the sampling step length in meters. + * @param precision the maximum sampling error in meters. + * @return the Position found or null. + */ + public static Position intersectRayWithTerrain(Globe globe, Vec4 origin, Vec4 direction, + double sampleLength, double precision, Position pos) + { + if (globe == null) + { + String msg = Logging.getMessage("nullValue.GlobeIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + if (origin == null || direction == null) + { + String msg = Logging.getMessage("nullValue.Vec4IsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + if (sampleLength < 0) + { + String msg = Logging.getMessage("generic.ArgumentOutOfRange", sampleLength); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + if (precision < 0) + { + String msg = Logging.getMessage("generic.ArgumentOutOfRange", precision); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + direction = direction.normalize3(); + + // Check whether we intersect the globe at it's highest elevation + Intersection inters[] = globe.intersect(new Line(origin, direction), globe.getMaxElevation()); + if (inters != null) + { + // Sort out intersection points and direction + Vec4 p1 = inters[0].getIntersectionPoint(); + Vec4 p2 = null; + if (p1.subtract3(origin).dot3(direction) < 0) + p1 = null; // wrong direction + if (inters.length == 2) + { + p2 = inters[1].getIntersectionPoint(); + if (p2.subtract3(origin).dot3(direction) < 0) + p2 = null; // wrong direction + } + + if (p1 == null && p2 == null) // both points in wrong direction + return null; + + if (p1 != null && p2 != null) + { + // Outside sphere move to closest point + if (origin.distanceTo3(p1) > origin.distanceTo3(p2)) + { + // switch p1 and p2 + Vec4 temp = p2; + p2 = p1; + p1 = temp; + } + } + else + { + // single point in right direction: inside sphere + p2 = p2 == null ? p1 : p2; + p1 = origin; + } + + // Sample between p1 and p2 + Vec4 point = new Vec4(); + intersectSegmentWithTerrain(globe, p1, p2, sampleLength, precision, point); + if (point != null) + pos = globe.computePositionFromPoint(point); + } + return pos; + } + + /** + * Compute the intersection Vec4 point of the globe terrain with a line segment + * defined between two points. Uses the default sample length and result precision. + * @param globe the globe to intersect with. + * @param p1 segment start point. + * @param p2 segment end point. + * @return the Vec4 point found or null. + */ + public static Vec4 intersectSegmentWithTerrain(Globe globe, Vec4 p1, Vec4 p2, Vec4 out) + { + return intersectSegmentWithTerrain(globe, p1, p2, defaultSampleLength, defaultPrecision, out); + } + + /** + * Compute the intersection Vec4 point of the globe terrain with the a segment + * defined between two points. Uses the given sample length and result precision. + * @param globe the globe to intersect with. + * @param p1 segment start point. + * @param p2 segment end point. + * @param sampleLength the sampling step length in meters. + * @param precision the maximum sampling error in meters. + * @return the Vec4 point found or null. + */ + public static Vec4 intersectSegmentWithTerrain(Globe globe, Vec4 p1, Vec4 p2, + double sampleLength, double precision, Vec4 point) + { + if (globe == null) + { + String msg = Logging.getMessage("nullValue.GlobeIsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + if (p1 == null || p2 == null) + { + String msg = Logging.getMessage("nullValue.Vec4IsNull"); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + if (sampleLength < 0) + { + String msg = Logging.getMessage("generic.ArgumentOutOfRange", sampleLength); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + if (precision < 0) + { + String msg = Logging.getMessage("generic.ArgumentOutOfRange", precision); + Logging.error(msg); + throw new IllegalArgumentException(msg); + } + + // Sample between p1 and p2 + Line ray = new Line(p1, p2.subtract3(p1).normalize3()); + double rayLength = p1.distanceTo3(p2); + double sampledDistance = 0; + Vec4 sample = p1; + Vec4 lastSample = null; + while (sampledDistance <= rayLength) + { + Position samplePos = globe.computePositionFromPoint(sample); + if (samplePos.elevation <= globe.getElevation(samplePos.getLatitude(), samplePos.getLongitude())) + { + // Below ground, intersection found + point = sample; + break; + } + if (sampledDistance >= rayLength) + break; // break after last sample + // Keep sampling + lastSample = sample; + sampledDistance = Math.min(sampledDistance + sampleLength, rayLength); + ray.getPointAt(sampledDistance, sample); + } + + // Recurse for more precision if needed + if (point != null && sampleLength > precision && lastSample != null) + intersectSegmentWithTerrain(globe, lastSample, point, sampleLength / 10, precision, point); + + return point; + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/util/StatusBar.java b/WorldWindAndroid/src/gov/nasa/worldwind/util/StatusBar.java new file mode 100644 index 0000000..1974e32 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/util/StatusBar.java @@ -0,0 +1,325 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.util; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.RelativeLayout; +import android.widget.TextView; +import gov.nasa.worldwind.R; +import gov.nasa.worldwind.WorldWind; +import gov.nasa.worldwind.WorldWindow; +import gov.nasa.worldwind.event.PositionEvent; +import gov.nasa.worldwind.event.PositionListener; +import gov.nasa.worldwind.event.RenderingEvent; +import gov.nasa.worldwind.event.RenderingListener; +import gov.nasa.worldwind.geom.Angle; +import gov.nasa.worldwind.geom.Position; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.net.URL; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @author tag + * @version $Id$ + */ +public class StatusBar extends RelativeLayout implements PositionListener, RenderingListener +{ + // Units constants TODO: Replace with UnitsFormat + public final static String UNIT_METRIC = "gov.nasa.worldwind.StatusBar.Metric"; + public final static String UNIT_IMPERIAL = "gov.nasa.worldwind.StatusBar.Imperial"; + + protected static final int MAX_ALPHA = 254; + + private WorldWindow eventSource; + private String elevationUnit = UNIT_METRIC; + private String angleFormat = Angle.ANGLE_FORMAT_DD; + + protected TextView onlineDisplay; + protected TextView latDisplay; + protected TextView lonDisplay; + protected TextView altDisplay; + protected TextView eleDisplay; + protected TextView heartBeat; + protected Drawable offlineDrawable = getResources().getDrawable(R.drawable.ic_action_network_wifi_off_status); + protected Drawable onlineDrawable = getResources().getDrawable(R.drawable.ic_action_network_wifi_status); + protected Drawable downloadDrawable = getResources().getDrawable(R.drawable.ic_action_download_status); + + protected AtomicBoolean showNetworkStatus = new AtomicBoolean(true); + protected AtomicBoolean isNetworkAvailable = new AtomicBoolean(true); + protected Thread netCheckThread; + protected ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); + + private static final int WHAT_NETWORK_STATUS = 0; + private static final int WHAT_ELEVATION_CHANGE = 1; + private static final int WHAT_POSITION_CHANGE = 2; + + + private Handler uiHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch(msg.what) { + case WHAT_NETWORK_STATUS: + heartBeat.setVisibility(isShowNetworkStatus() ? VISIBLE : INVISIBLE); + if (!isShowNetworkStatus()) + return; + + if (!isNetworkAvailable.get()) { + heartBeat.setText(Logging.getMessage("term.NoNetwork")); + heartBeat.setTextColor(Color.argb(MAX_ALPHA, 255, 0, 0)); + heartBeat.setCompoundDrawables(offlineDrawable, null, null, null); + return; + } + + int alpha = Color.alpha(heartBeat.getCurrentTextColor()); + if (isNetworkAvailable.get() && WorldWind.getRetrievalService().hasActiveTasks()) { + heartBeat.setText(Logging.getMessage("term.Downloading")); + heartBeat.setCompoundDrawables(onlineDrawable, null, downloadDrawable, null); + if (alpha >= MAX_ALPHA) + alpha = MAX_ALPHA; + else + alpha = alpha < 16 ? 16 : Math.min(MAX_ALPHA, alpha + 20); + } else { + alpha = Math.max(0, alpha - 20); + heartBeat.setCompoundDrawables(onlineDrawable, null, null, null); + } + heartBeat.setTextColor(Color.argb(alpha, 255, 0, 0)); + break; + case WHAT_POSITION_CHANGE: +// Position newPos = msg.getData().getParcelable("position"); + Position newPos = (Position) msg.obj; + if (newPos != null) + { + String las = makeAngleDescription("Lat", newPos.getLatitude()); + String los = makeAngleDescription("Lon", newPos.getLongitude()); + String els = makeCursorElevationDescription( + eventSource.getModel().getGlobe().getElevation(newPos.getLatitude(), newPos.getLongitude())); + latDisplay.setText(las); + lonDisplay.setText(los); + eleDisplay.setText(els); + } + else + { + latDisplay.setText(""); + lonDisplay.setText(Logging.getMessage("term.OffGlobe")); + eleDisplay.setText(""); + } + break; + case WHAT_ELEVATION_CHANGE: + if (eventSource.getView() != null && eventSource.getView().getEyePosition() != null) + altDisplay.setText(makeEyeAltitudeDescription( + eventSource.getView().getEyePosition().elevation)); + else + altDisplay.setText(Logging.getMessage("term.Altitude")); + break; + } + } + }; + + public StatusBar(Context context, AttributeSet attr) { + this(context, attr, 0); + } + + public StatusBar(Context context, AttributeSet attr, int defStyle) + { + super(context, attr, defStyle); + + LayoutInflater.from(context).inflate(R.layout.statusbar, this, true); + + onlineDisplay = (TextView) findViewById(R.id.online); + latDisplay = (TextView) findViewById(R.id.latitude); + lonDisplay = (TextView) findViewById(R.id.longitude); + lonDisplay.setText(Logging.getMessage("term.OffGlobe")); + altDisplay = (TextView) findViewById(R.id.altitude); + eleDisplay = (TextView) findViewById(R.id.elevation); + heartBeat = (TextView) findViewById(R.id.heartBeat); + heartBeat.setText(Logging.getMessage("term.Downloading")); + heartBeat.setTextColor(Color.RED); + + WorldWind.getNetworkStatus().addPropertyChangeListener(NetworkStatus.HOST_UNAVAILABLE, + new PropertyChangeListener() + { + public void propertyChange(PropertyChangeEvent evt) + { + Object nv = evt.getNewValue(); + String message = Logging.getMessage("NetworkStatus.UnavailableHost", + nv != null && nv instanceof URL ? ((URL) nv).getHost() : "Unknown"); + Logging.info(message); + } + }); + + WorldWind.getNetworkStatus().addPropertyChangeListener(NetworkStatus.HOST_AVAILABLE, + new PropertyChangeListener() + { + public void propertyChange(PropertyChangeEvent evt) + { + Object nv = evt.getNewValue(); + String message = Logging.getMessage("NetworkStatus.HostNowAvailable", + nv != null && nv instanceof URL ? ((URL) nv).getHost() : "Unknown"); + Logging.info(message); + } + }); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if(isShowNetworkStatus()) + netCheckThread = startNetCheckThread(); + executorService.scheduleAtFixedRate + (new Runnable() { + public void run() { + uiHandler.sendEmptyMessage(WHAT_NETWORK_STATUS); + } + }, 0, 100, TimeUnit.MILLISECONDS); + } + + @Override + protected void onDetachedFromWindow() { + executorService.shutdown(); + if(netCheckThread!=null) + netCheckThread.interrupt(); + super.onDetachedFromWindow(); + } + + protected NetworkCheckThread startNetCheckThread() + { + NetworkCheckThread nct = new NetworkCheckThread(this.showNetworkStatus, this.isNetworkAvailable, null); + nct.setDaemon(true); + nct.start(); + + return nct; + } + + public void setEventSource(WorldWindow newEventSource) + { + if (this.eventSource != null) + { + this.eventSource.removePositionListener(this); + this.eventSource.removeRenderingListener(this); + } + + if (newEventSource != null) + { + newEventSource.addPositionListener(this); + newEventSource.addRenderingListener(this); + } + + this.eventSource = newEventSource; + } + + public boolean isShowNetworkStatus() + { + return showNetworkStatus.get(); + } + + public void setShowNetworkStatus(boolean showNetworkStatus) + { + this.showNetworkStatus.set(showNetworkStatus); + if (this.netCheckThread != null) + this.netCheckThread.interrupt(); + this.netCheckThread = showNetworkStatus ? this.startNetCheckThread() : null; + } + + public void moved(PositionEvent event) + { + this.handleCursorPositionChange(event); + } + + public WorldWindow getEventSource() + { + return this.eventSource; + } + + public String getElevationUnit() + { + return this.elevationUnit; + } + + public void setElevationUnit(String unit) + { + if (unit == null) + { + String message = Logging.getMessage("nullValue.StringIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.elevationUnit = unit; + } + + public String getAngleFormat() + { + return this.angleFormat; + } + + public void setAngleFormat(String format) + { + if (format == null) + { + String message = Logging.getMessage("nullValue.StringIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.angleFormat = format; + } + + protected String makeCursorElevationDescription(double metersElevation) + { + String s; + String elev = Logging.getMessage("term.Elev"); + if (UNIT_IMPERIAL.equals(elevationUnit)) + s = String.format(elev + " %,7d feet", (int) (WWMath.convertMetersToFeet(metersElevation))); + else // Default to metric units. + s = String.format(elev + " %,7d meters", (int) metersElevation); + return s; + } + + protected String makeEyeAltitudeDescription(double metersAltitude) + { + String s; + String altitude = Logging.getMessage("term.Altitude"); + if (UNIT_IMPERIAL.equals(elevationUnit)) + s = String.format(altitude + " %,7d mi", (int) Math.round(WWMath.convertMetersToMiles(metersAltitude))); + else // Default to metric units. + s = String.format(altitude + " %,7d m", (int) Math.round(metersAltitude)); + return s; + } + + protected String makeAngleDescription(String label, Angle angle) + { + String s; + if (Angle.ANGLE_FORMAT_DMS.equals(angleFormat)) + s = String.format("%s %s", label, angle.toDMSString()); + else + s = String.format("%s %7.4f\u00B0", label, angle.degrees); + return s; + } + + protected void handleCursorPositionChange(final PositionEvent event) + { + uiHandler.obtainMessage(WHAT_POSITION_CHANGE, event).sendToTarget(); + } + + public void stageChanged(RenderingEvent event) + { + if (!event.getStage().equals(RenderingEvent.AFTER_RENDERING)) + return; + + uiHandler.sendEmptyMessage(WHAT_ELEVATION_CHANGE); + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/util/SurfaceTileDrawContext.java b/WorldWindAndroid/src/gov/nasa/worldwind/util/SurfaceTileDrawContext.java new file mode 100644 index 0000000..73d131e --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/util/SurfaceTileDrawContext.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.util; + +import android.graphics.Rect; +import gov.nasa.worldwind.geom.LatLon; +import gov.nasa.worldwind.geom.Matrix; +import gov.nasa.worldwind.geom.Sector; + +/** + * SurfaceTileDrawContext contains the context needed to render to an off-screen surface tile. SurfaceTileDrawContext is + * defined by a geographic Sector and a corresponding tile viewport. The Sector maps geographic coordinates to pixels in + * an abstract off-screen tile. + * + * @author dcollins + * @version $Id: SurfaceTileDrawContext.java 1171 2013-02-11 21:45:02Z dcollins $ + */ +public class SurfaceTileDrawContext +{ + protected Sector sector; + protected Rect viewport; + protected Matrix projection; + protected Matrix modelview; + + /** + * Constructs a SurfaceTileDrawContext with a specified surface Sector and viewport. The Sector defines the + * context's geographic extent, and the viewport defines the context's corresponding viewport in pixels. + * + * @param sector the context's Sector. + * @param viewport the context's viewport in pixels. + * + * @throws IllegalArgumentException if either the sector or viewport are null, or if the viewport width or height is + * less than or equal to zero. + */ + public SurfaceTileDrawContext(Sector sector, Rect viewport, Matrix projectionMatrix) + { + if (sector == null) + { + String message = Logging.getMessage("nullValue.SectorIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (viewport == null) + { + String message = Logging.getMessage("nullValue.ViewportIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (viewport.width() <= 0) + { + String message = Logging.getMessage("Geom.WidthInvalid"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (viewport.height() <= 0) + { + String message = Logging.getMessage("Geom.HeightInvalid"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.sector = sector; + this.viewport = viewport; + this.modelview = Matrix.fromGeographicToViewport(sector, viewport.left, viewport.top, + viewport.width(), viewport.height()); + this.projection = projectionMatrix; + } + + /** + * Constructs a SurfaceTileDrawContext with a specified surface Sector and viewport dimension. The Sector defines + * the context's geographic extent, and the viewport defines the context's corresponding viewport's dimension in + * pixels. + * + * @param sector the context's Sector. + * @param viewportWidth the context's viewport width in pixels. + * @param viewportHeight the context's viewport height in pixels. + * @param projectionMatrix the projection matrix + * + * @throws IllegalArgumentException if the sector is null, or if the viewport width or height is less than or equal + * to zero. + */ + public SurfaceTileDrawContext(Sector sector, int viewportWidth, int viewportHeight, Matrix projectionMatrix) + { + this(sector, new Rect(0, 0, viewportWidth, viewportHeight), projectionMatrix); + } + + /** + * Returns the context's Sector. + * + * @return the context's Sector. + */ + public Sector getSector() + { + return this.sector; + } + + /** + * Returns the context's viewport. + * + * @return the context's viewport. + */ + public Rect getViewport() + { + return this.viewport; + } + + /** + * Returns a projection Matrix. + * + * @return projection matrix + */ + public Matrix getProjectionMatrix() + { + return this.projection; + } + + /** + * Returns a Matrix mapping geographic coordinates to pixels in the off-screen tile. + * + * @return Matrix mapping geographic coordinates to tile coordinates. + */ + public Matrix getModelviewMatrix() + { + return this.modelview; + } + + /** + * Returns a Matrix mapping geographic coordinates to pixels in the off-screen tile. The reference location defines + * the geographic coordinate origin. + * + * @param referenceLocation the geographic coordinate origin. + * + * @return Matrix mapping geographic coordinates to tile coordinates. + * + * @throws IllegalArgumentException if the reference location is null. + */ + public Matrix getModelviewMatrix(LatLon referenceLocation) + { + if (referenceLocation == null) + { + String message = Logging.getMessage("nullValue.LatLonIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + return this.modelview.multiply( + Matrix.fromTranslation(referenceLocation.getLongitude().degrees, referenceLocation.getLatitude().degrees, + 0)); + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/util/SystemPropertiesProxy.java b/WorldWindAndroid/src/gov/nasa/worldwind/util/SystemPropertiesProxy.java new file mode 100644 index 0000000..d690e62 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/util/SystemPropertiesProxy.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ + +package gov.nasa.worldwind.util;/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ + + +import java.lang.reflect.Method; + +/** + * Access android.os.SystemProperties via reflection. + * + * http://stackoverflow.com/questions/2641111/where-is-android-os-systemproperties + */ +public class SystemPropertiesProxy +{ + + /** + * This class cannot be instantiated + */ + private SystemPropertiesProxy(){ + + } + + /** + * Get the value for the given key. + * @return an empty string if the key isn't found + * @throws IllegalArgumentException if the key exceeds 32 characters + */ + @SuppressWarnings({"rawtypes","unchecked"}) + public static String get(String key) throws IllegalArgumentException { + + String ret= ""; + + try{ + Class SystemProperties = Class.forName("android.os.SystemProperties"); + + //Parameters Types + Class[] paramTypes= new Class[1]; + paramTypes[0]= String.class; + + Method get = SystemProperties.getMethod("get", paramTypes); + + //Parameters + Object[] params= new Object[1]; + params[0]= new String(key); + + ret= (String) get.invoke(SystemProperties, params); + + }catch( IllegalArgumentException iAE ){ + throw iAE; + }catch( Exception e ){ + ret= ""; + //TODO + } + + return ret; + + } + + /** + * Get the value for the given key. + * @return if the key isn't found, return def if it isn't null, or an empty string otherwise + * @throws IllegalArgumentException if the key exceeds 32 characters + */ + @SuppressWarnings({"rawtypes","unchecked"}) + public static String get(String key, String def) throws IllegalArgumentException { + + String ret= def; + + try{ + Class SystemProperties = Class.forName("android.os.SystemProperties"); + + //Parameters Types + Class[] paramTypes= new Class[2]; + paramTypes[0]= String.class; + paramTypes[1]= String.class; + + Method get = SystemProperties.getMethod("get", paramTypes); + + //Parameters + Object[] params= new Object[2]; + params[0]= new String(key); + params[1]= new String(def); + + ret= (String) get.invoke(SystemProperties, params); + + }catch( IllegalArgumentException iAE ){ + throw iAE; + }catch( Exception e ){ + ret= def; + //TODO + } + + return ret; + + } + + /** + * Get the value for the given key, and return as an integer. + * @param key the key to lookup + * @param def a default value to return + * @return the key parsed as an integer, or def if the key isn't found or + * cannot be parsed + * @throws IllegalArgumentException if the key exceeds 32 characters + */ + @SuppressWarnings({"rawtypes","unchecked"}) + public static Integer getInt(String key, int def) throws IllegalArgumentException { + + Integer ret= def; + + try{ + Class SystemProperties = Class.forName("android.os.SystemProperties"); + + //Parameters Types + Class[] paramTypes= new Class[2]; + paramTypes[0]= String.class; + paramTypes[1]= int.class; + + Method getInt = SystemProperties.getMethod("getInt", paramTypes); + + //Parameters + Object[] params= new Object[2]; + params[0]= new String(key); + params[1]= new Integer(def); + + ret= (Integer) getInt.invoke(SystemProperties, params); + + }catch( IllegalArgumentException iAE ){ + throw iAE; + }catch( Exception e ){ + ret= def; + //TODO + } + + return ret; + + } + + /** + * Get the value for the given key, and return as a long. + * @param key the key to lookup + * @param def a default value to return + * @return the key parsed as a long, or def if the key isn't found or + * cannot be parsed + * @throws IllegalArgumentException if the key exceeds 32 characters + */ + @SuppressWarnings({"rawtypes","unchecked"}) + public static Long getLong(String key, long def) throws IllegalArgumentException { + + Long ret= def; + + try{ + Class SystemProperties= Class.forName("android.os.SystemProperties"); + + //Parameters Types + Class[] paramTypes= new Class[2]; + paramTypes[0]= String.class; + paramTypes[1]= long.class; + + Method getLong = SystemProperties.getMethod("getLong", paramTypes); + + //Parameters + Object[] params= new Object[2]; + params[0]= new String(key); + params[1]= new Long(def); + + ret= (Long) getLong.invoke(SystemProperties, params); + + }catch( IllegalArgumentException iAE ){ + throw iAE; + }catch( Exception e ){ + ret= def; + //TODO + } + + return ret; + + } + + /** + * Get the value for the given key, returned as a boolean. + * Values 'n', 'no', '0', 'false' or 'off' are considered false. + * Values 'y', 'yes', '1', 'true' or 'on' are considered true. + * (case insensitive). + * If the key does not exist, or has any other value, then the default + * result is returned. + * @param key the key to lookup + * @param def a default value to return + * @return the key parsed as a boolean, or def if the key isn't found or is + * not able to be parsed as a boolean. + * @throws IllegalArgumentException if the key exceeds 32 characters + */ + @SuppressWarnings({"rawtypes","unchecked"}) + public static Boolean getBoolean(String key, boolean def) throws IllegalArgumentException { + + Boolean ret= def; + + try{ + Class SystemProperties = Class.forName("android.os.SystemProperties"); + + //Parameters Types + Class[] paramTypes= new Class[2]; + paramTypes[0]= String.class; + paramTypes[1]= boolean.class; + + Method getBoolean = SystemProperties.getMethod("getBoolean", paramTypes); + + //Parameters + Object[] params= new Object[2]; + params[0]= new String(key); + params[1]= new Boolean(def); + + ret= (Boolean) getBoolean.invoke(SystemProperties, params); + + }catch( IllegalArgumentException iAE ){ + throw iAE; + }catch( Exception e ){ + ret= def; + //TODO + } + + return ret; + + } + + /** + * Set the value for the given key. + * @throws IllegalArgumentException if the key exceeds 32 characters + * @throws IllegalArgumentException if the value exceeds 92 characters + */ + @SuppressWarnings({"rawtypes","unchecked"}) + public static void set(String key, String val) throws IllegalArgumentException { + + try{ + Class SystemProperties = Class.forName("android.os.SystemProperties"); + + //Parameters Types + Class[] paramTypes= new Class[2]; + paramTypes[0]= String.class; + paramTypes[1]= String.class; + + Method set = SystemProperties.getMethod("set", paramTypes); + + //Parameters + Object[] params= new Object[2]; + params[0]= new String(key); + params[1]= new String(val); + + set.invoke(SystemProperties, params); + + }catch( IllegalArgumentException iAE ){ + throw iAE; + }catch( Exception e ){ + //TODO + } + + } +} \ No newline at end of file diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/util/Tile.java b/WorldWindAndroid/src/gov/nasa/worldwind/util/Tile.java index bbc9dfc..731b8ae 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/util/Tile.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/util/Tile.java @@ -29,8 +29,8 @@ * @version $Id: Tile.java 842 2012-10-09 23:46:47Z tgaskins $ */ public class Tile implements Cacheable { - public interface TileFactory { - Tile createTile(Sector sector, Level level, int row, int column); + public interface TileFactory { + T createTile(Sector sector, Level level, int row, int column); } protected Sector sector; @@ -67,25 +67,7 @@ public interface TileFactory { * if sector or level is null. */ public Tile(Sector sector, Level level, int row, int column) { - if (sector == null) { - String msg = Logging.getMessage("nullValue.SectorIsNull"); - Logging.error(msg); - throw new IllegalArgumentException(msg); - } - - if (level == null) { - String msg = Logging.getMessage("nullValue.LevelIsNull"); - Logging.error(msg); - throw new IllegalArgumentException(msg); - } - - this.sector = sector; - this.level = level; - this.row = row; - this.column = column; - this.cacheName = null; - this.tileKey = new TileKey(this); - this.path = null; + this(sector, level, row, column, null); } /** @@ -546,7 +528,7 @@ public static void createTilesForLevel(Level level, Sector sector, TileFactory f } } - public static void createTilesForLevel(Level level, Sector sector, TileFactory factory, List result, LatLon origin) { + public static void createTilesForLevel(Level level, Sector sector, TileFactory factory, List result, LatLon origin) { if (level == null) { String msg = Logging.getMessage("nullValue.LevelIsNull"); Logging.error(msg); diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/util/WWIO.java b/WorldWindAndroid/src/gov/nasa/worldwind/util/WWIO.java index 1cb5582..0c4ee65 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/util/WWIO.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/util/WWIO.java @@ -5,18 +5,15 @@ */ package gov.nasa.worldwind.util; +import android.content.Context; import gov.nasa.worldwind.Configuration; import gov.nasa.worldwind.avlist.AVKey; +import gov.nasa.worldwind.cache.GpuResourceCache; import gov.nasa.worldwind.exception.WWRuntimeException; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.Closeable; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; +import gov.nasa.worldwind.render.GpuProgram; + +import java.io.*; +import java.lang.ref.WeakReference; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.SocketAddress; @@ -31,8 +28,7 @@ import java.nio.channels.FileLock; import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; -import java.util.HashMap; -import java.util.Map; +import java.util.*; /** * @author dcollins @@ -45,6 +41,7 @@ public class WWIO { protected static final int DEFAULT_PAGE_SIZE = 2 << 15; protected static final Map mimeTypeToSuffixMap = new HashMap(); protected static final Map suffixToMimeTypeMap = new HashMap(); + protected static WeakReference context; static { mimeTypeToSuffixMap.put("application/acad", "dwg"); @@ -74,6 +71,7 @@ public class WWIO { mimeTypeToSuffixMap.put("image/jpeg", "jpg"); mimeTypeToSuffixMap.put("image/jpg", "jpg"); mimeTypeToSuffixMap.put("image/png", "png"); + mimeTypeToSuffixMap.put("image/pkm", "pkm"); mimeTypeToSuffixMap.put("image/svg+xml", "svg"); mimeTypeToSuffixMap.put("image/tiff", "tif"); mimeTypeToSuffixMap.put("image/x-imagewebserver-ecw", "ecw"); @@ -113,6 +111,7 @@ public class WWIO { suffixToMimeTypeMap.put("jp2", "image/jp2"); suffixToMimeTypeMap.put("jpeg", "image/jpeg"); suffixToMimeTypeMap.put("jpg", "image/jpeg"); + suffixToMimeTypeMap.put("pkm", "image/pkm"); suffixToMimeTypeMap.put("kml", "application/vnd.google-earth.kml+xml"); suffixToMimeTypeMap.put("kmz", "application/vnd.google-earth.kmz"); suffixToMimeTypeMap.put("mid", "audio/x-midi"); @@ -141,6 +140,14 @@ public class WWIO { suffixToMimeTypeMap.put("zip", "application/zip"); } + public static void setContext(Context c) { + context = new WeakReference(c); + } + + public static Context getContext() { + return context.get(); + } + public static String formPath(String... pathParts) { StringBuilder sb = new StringBuilder(); @@ -203,28 +210,27 @@ public static InputStream getInputStreamFromByteBuffer(ByteBuffer buffer) { return new ByteArrayInputStream(byteArray); } - public static Object getFileOrResourceAsStream(String path, Class c) { + public static BufferedInputStream getFileOrResourceAsBufferedStream(String path, Class c) { + InputStream stream = getFileOrResourceAsStream(path, c); + if(stream!=null && !(stream instanceof BufferedInputStream)) + stream = new BufferedInputStream(stream); + return (BufferedInputStream)stream; + } + + public static InputStream getFileOrResourceAsStream(String path, Class c) { if (path == null) { String msg = Logging.getMessage("nullValue.PathIsNull"); Logging.error(msg); throw new IllegalStateException(msg); } - File file = new File(path); - if (file.exists()) { - try { - return new FileInputStream(file); - } catch (Exception e) { - return e; - } - } - - if (c == null) c = WWIO.class; - try { - return c.getResourceAsStream("/" + path); + if (new File(path).exists()) + return new FileInputStream(new File(path)); + + return (c==null ? WWIO.class : c).getResourceAsStream("/" + path); } catch (Exception e) { - return e; + throw new WWRuntimeException(Logging.getMessage("generic.UnableToOpenPath", path), e); } } @@ -443,7 +449,9 @@ public static InputStream openStream(Object source) { } else if (source instanceof InputStream) { return (InputStream) source; } else if (source instanceof File) { - return openFileOrResourceStream(((File) source).getPath(), null); + return openFileOrResourceStream(((File) source).getPath(), null); + } else if (source instanceof Integer && context!=null) { + return getContext().getResources().openRawResource((Integer) source); } else if (!(source instanceof String)) { throw new IllegalArgumentException(Logging.getMessage("generic.SourceTypeUnrecognized", source)); } @@ -456,6 +464,13 @@ public static InputStream openStream(Object source) { return openFileOrResourceStream(sourceName, null); } + public static BufferedInputStream openBufferedStream(Object source) { + InputStream stream = openStream(source); + if(stream!=null && !(stream instanceof BufferedInputStream)) + stream = new BufferedInputStream(stream); + return (BufferedInputStream)stream; + } + /** * Opens a file located via an absolute path or a path relative to the classpath. * @@ -477,14 +492,7 @@ public static InputStream openFileOrResourceStream(String path, Class c) { throw new IllegalArgumentException(msg); } - Object streamOrException = WWIO.getFileOrResourceAsStream(path, c); - - if (streamOrException instanceof Exception) { - String msg = Logging.getMessage("generic.UnableToOpenPath", path); - throw new WWRuntimeException(msg, (Exception) streamOrException); - } - - return (InputStream) streamOrException; + return WWIO.getFileOrResourceAsStream(path, c); } public static InputStream openURLStream(URL url) { @@ -824,6 +832,15 @@ public static boolean isFileOutOfDate(URL url, long expiryTime) { } } + /** + * Get file extension from filename + * @param filename the filename + * @return the extension, without period character + */ + public static String getFileExtension(String filename) { + return filename.substring(filename.length()-3); + } + public static Proxy configureProxy() { String proxyHost = Configuration.getStringValue(AVKey.URL_PROXY_HOST); if (proxyHost == null) return null; @@ -951,4 +968,24 @@ public static File makeTempDir() throws IOException { return temp; } + + public static GpuProgram getGpuProgram(GpuResourceCache cache, Object programKey, int vert, int frag) { + GpuProgram program = cache.getProgram(programKey); + + if (program == null) { + try { + GpuProgram.GpuProgramSource source = GpuProgram.readProgramSource(vert, frag); + program = new GpuProgram(source); + cache.put(programKey, program); + } catch (Exception e) { + String msg = Logging.getMessage("GL.ExceptionLoadingProgram", + getContext().getResources().getResourceName(vert), + getContext().getResources().getResourceName(frag)); + Logging.error(msg); + return null; + } + } + + return program; + } } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/util/WWMath.java b/WorldWindAndroid/src/gov/nasa/worldwind/util/WWMath.java index b31f233..faf6ccc 100644 --- a/WorldWindAndroid/src/gov/nasa/worldwind/util/WWMath.java +++ b/WorldWindAndroid/src/gov/nasa/worldwind/util/WWMath.java @@ -5,8 +5,11 @@ */ package gov.nasa.worldwind.util; +import gov.nasa.worldwind.View; +import gov.nasa.worldwind.WorldWindowGLSurfaceView; import gov.nasa.worldwind.geom.*; import gov.nasa.worldwind.globes.Globe; +import gov.nasa.worldwind.render.DrawContext; import java.nio.FloatBuffer; import java.util.*; @@ -20,11 +23,23 @@ public class WWMath { /** The ratio of milliseconds per second. Used to convert time in seconds to time in milliseconds. */ - protected static final double SECOND_TO_MILLIS = 1000.0; + public static final double SECOND_TO_MILLIS = 1000.0; /** The ratio of milliseconds per minute. Used to convert time in minutes to time in milliseconds. */ - protected static final double MINUTE_TO_MILLIS = 60.0 * SECOND_TO_MILLIS; + public static final double MINUTE_TO_MILLIS = 60.0 * SECOND_TO_MILLIS; /** The ratio of milliseconds per hour. Used to convert time in hours to time in milliseconds. */ - protected static final double HOUR_TO_MILLIS = 60.0 * MINUTE_TO_MILLIS; + public static final double HOUR_TO_MILLIS = 60.0 * MINUTE_TO_MILLIS; + + public static final double METERS_TO_KILOMETERS = 1e-3; + public static final double METERS_TO_MILES = 0.000621371192; + public static final double METERS_TO_NAUTICAL_MILES = 0.000539956803; + public static final double METERS_TO_YARDS = 1.0936133; + public static final double METERS_TO_FEET = 3.280839895; + + public static final double KILOMETERS_TO_METERS = 1/METERS_TO_KILOMETERS; + public static final double MILES_TO_METERS = 1/METERS_TO_MILES; + public static final double NAUTICAL_MILES_TO_METERS = 1/METERS_TO_NAUTICAL_MILES; + public static final double YARDS_TO_METERS = 1/METERS_TO_YARDS; + public static final double FEET_TO_METERS = 1/METERS_TO_FEET; // Temporary properties used to avoid constant reallocation of primitive types. protected static Vec4 point1 = new Vec4(); @@ -32,6 +47,34 @@ public class WWMath protected static Matrix matrix = Matrix.fromIdentity(); protected static Matrix matrixInv = Matrix.fromIdentity(); + /** + * Clamps a value to a given range. + * + * @param v the value to clamp. + * @param min the floor. + * @param max the ceiling + * + * @return the nearest value such that min <= v <= max. + */ + public static double clamp(double v, double min, double max) + { + return v < min ? min : v > max ? max : v; + } + + /** + * Clamps an integer value to a given range. + * + * @param v the value to clamp. + * @param min the floor. + * @param max the ceiling + * + * @return the nearest value such that min <= v <= max. + */ + public static int clamp(int v, int min, int max) + { + return v < min ? min : v > max ? max : v; + } + /** * Converts time in seconds to time in milliseconds. * @@ -68,6 +111,73 @@ public static double convertHoursToMillis(double hours) return hours * HOUR_TO_MILLIS; } + /** + * converts meters to feet. + * + * @param meters the value in meters. + * + * @return the value converted to feet. + */ + public static double convertMetersToFeet(double meters) + { + return (meters * METERS_TO_FEET); + } + + /** + * converts meters to miles. + * + * @param meters the value in meters. + * + * @return the value converted to miles. + */ + public static double convertMetersToMiles(double meters) + { + return (meters * METERS_TO_MILES); + } + + /** + * Converts distance in feet to distance in meters. + * + * @param feet the distance in feet. + * + * @return the distance converted to meters. + */ + public static double convertFeetToMeters(double feet) + { + return (feet / METERS_TO_FEET); + } + + /** + * Returns the distance in model coordinates from the {@link gov.nasa.worldwind.View} eye point to the specified + * {@link gov.nasa.worldwind.geom.Extent}. If the View eye point is inside the extent, this returns 0. + * + * @param dc the {@link gov.nasa.worldwind.render.DrawContext} which the View eye point is obtained from. + * @param extent the extent to compute the distance from. + * + * @return the distance from the View eye point to the extent, in model coordinates. + * + * @throws IllegalArgumentException if either the DrawContext or the extent is null. + */ + public static double computeDistanceFromEye(DrawContext dc, Extent extent) + { + if (dc == null) + { + String message = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (extent == null) + { + String message = Logging.getMessage("nullValue.ExtentIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + double distance = dc.getView().getEyePoint().distanceTo3(extent.getCenter()) - extent.getRadius(); + return (distance < 0d) ? 0d : distance; + } + /** * Computes the distance to the horizon from a viewer at the specified elevation. Only the globe's ellipsoid is * considered; terrain elevations are not incorporated. This returns zero if the specified elevation is less than or @@ -92,7 +202,7 @@ public static double computeHorizonDistance(Globe globe, double elevation) if (elevation <= 0) return 0; - return Math.sqrt(elevation * (2 * globe.getRadius() + elevation)); + return Math.sqrt(elevation * (2 * globe.getRadius() + elevation)); } /** @@ -331,6 +441,220 @@ public static boolean computeRayFromScreenPoint(double x, double y, Matrix model return true; } + /** + * Computes the area in square pixels of a sphere after it is projected into the specified view's + * viewport. The returned value is the screen area that the sphere covers in the infinite plane defined by the + * view's viewport. This area is not limited to the size of the view's viewport, and + * portions of the sphere are not clipped by the view's frustum. + *

    + * This returns zero if the specified radius is zero. + * + * @param view the View for which to compute a projected screen area. + * @param center the sphere's center point, in model coordinates. + * @param radius the sphere's radius, in meters. + * + * @return the projected screen area of the sphere in square pixels. + * + * @throws IllegalArgumentException if the view is null, if center is + * null, or if radius is less than zero. + */ + public static double computeSphereProjectedArea(View view, Vec4 center, double radius) + { + if (view == null) + { + String message = Logging.getMessage("nullValue.ViewIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (center == null) + { + String message = Logging.getMessage("nullValue.CenterIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (radius < 0) + { + String message = Logging.getMessage("Geom.RadiusIsNegative", radius); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (radius == 0) + return 0; + + // Compute the sphere's area by scaling its radius based on the sphere's depth in eye coordinates. This provides + // a good approximation of the sphere's projected area, but does not provide an exact value: the perspective + // projection of a sphere is an ellipse. + + // Compute the sphere's depth in eye coordinates by transforming the center point into eye coordinates and using + // its absolute z-value as the depth value. Then compute the radius in pixels by dividing the radius in meters + // by the number of meters per pixel at the sphere's depth. + double depth = Math.abs(center.transformBy4(view.getModelviewMatrix()).z); + double radiusInPixels = radius / view.computePixelSizeAtDistance(depth); + + return Math.PI * radiusInPixels * radiusInPixels; + } + + + + /** + * Computes a unit-length normal vector for a buffer of coordinate triples. The normal vector is computed from the + * first three non-colinear points in the buffer. + * + * @param coords the coordinates. This method returns null if this argument is null. + * @param stride the number of floats between successive points. 0 indicates that the points are arranged one + * immediately after the other. + * + * @return the computed unit-length normal vector, or null if a normal vector could not be computed. + */ + public static Vec4 computeBufferNormal(FloatBuffer coords, int stride) + { + Vec4[] verts = WWMath.findThreeIndependentVertices(coords, stride); + return verts != null ? WWMath.computeTriangleNormal(verts[0], verts[1], verts[2]) : null; + } + + /** + * Computes a unit-length normal vector for an array of coordinates. The normal vector is computed from the first + * three non-colinear points in the array. + * + * @param coords the coordinates. This method returns null if this argument is null. + * + * @return the computed unit-length normal vector, or null if a normal vector could not be computed. + */ + public static Vec4 computeArrayNormal(Vec4[] coords) + { + Vec4[] verts = WWMath.findThreeIndependentVertices(coords); + return verts != null ? WWMath.computeTriangleNormal(verts[0], verts[1], verts[2]) : null; + } + + /** + * Finds three non-colinear points in a buffer. + * + * @param coords the coordinates. This method returns null if this argument is null. + * @param stride the number of floats between successive points. 0 indicates that the points are arranged one + * immediately after the other. + * + * @return an array of three points, or null if three non-colinear points could not be found. + */ + public static Vec4[] findThreeIndependentVertices(FloatBuffer coords, int stride) + { + int xstride = stride > 0 ? stride : 3; + + if (coords == null || coords.limit() < 3 * xstride) + return null; + + Vec4 a = new Vec4(coords.get(0), coords.get(1), coords.get(2)); + Vec4 b = null; + Vec4 c = null; + + int k = xstride; + for (; k < coords.limit(); k += xstride) + { + b = new Vec4(coords.get(k), coords.get(k + 1), coords.get(k + 2)); + if (!(b.x == a.x && b.y == a.y && b.z == a.z)) + break; + b = null; + } + + if (b == null) + return null; + + for (k += xstride; k < coords.limit(); k += xstride) + { + c = new Vec4(coords.get(k), coords.get(k + 1), coords.get(k + 2)); + + // if c is not coincident with a or b, and the vectors ab and bc are not colinear, break and return a, b, c + if (!((c.x == a.x && c.y == a.y && c.z == a.z) || (c.x == b.x && c.y == b.y && c.z == b.z))) + { + if (!Vec4.areColinear(a, b, c)) + break; + } + + c = null; // reset c to signal failure to return statement below + } + + return c != null ? new Vec4[] {a, b, c} : null; + } + + /** + * Finds three non-colinear points in an array of points. + * + * @param coords the coordinates. This method returns null if this argument is null. + * + * @return an array of three points, or null if three non-colinear points could not be found. + */ + public static Vec4[] findThreeIndependentVertices(Vec4[] coords) + { + if (coords == null || coords.length < 3) + return null; + + Vec4 a = coords[0]; + Vec4 b = null; + Vec4 c = null; + + int k = 1; + for (; k < coords.length; k++) + { + b = coords[k]; + if (!(b.x == a.x && b.y == a.y && b.z == a.z)) + break; + b = null; + } + + if (b == null) + return null; + + for (; k < coords.length; k++) + { + c = coords[k]; + + // if c is not coincident with a or b, and the vectors ab and bc are not colinear, break and return a, b, c + if (!((c.x == a.x && c.y == a.y && c.z == a.z) || (c.x == b.x && c.y == b.y && c.z == b.z))) + { + if (!Vec4.areColinear(a, b, c)) + break; + } + + c = null; // reset c to signal failure to return statement below + } + + return c != null ? new Vec4[] {a, b, c} : null; + } + + /** + * Returns the normal vector corresponding to the triangle defined by three vertices (a, b, c). + * + * @param a the triangle's first vertex. + * @param b the triangle's second vertex. + * @param c the triangle's third vertex. + * + * @return the triangle's unit-length normal vector. + * + * @throws IllegalArgumentException if any of the specified vertices are null. + */ + public static Vec4 computeTriangleNormal(Vec4 a, Vec4 b, Vec4 c) + { + if (a == null || b == null || c == null) + { + String message = Logging.getMessage("nullValue.Vec4IsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + double x = ((b.y - a.y) * (c.z - a.z)) - ((b.z - a.z) * (c.y - a.y)); + double y = ((b.z - a.z) * (c.x - a.x)) - ((b.x - a.x) * (c.z - a.z)); + double z = ((b.x - a.x) * (c.y - a.y)) - ((b.y - a.y) * (c.x - a.x)); + + double length = (x * x) + (y * y) + (z * z); + if (length == 0d) + return new Vec4(x, y, z); + + length = Math.sqrt(length); + return new Vec4(x / length, y / length, z / length); + } + /** * Transforms the model coordinate point (x, y, z) to screen coordinates using the specified transform parameters. * This does not retain any reference to the specified parameters or modify them in any way. @@ -553,4 +877,163 @@ public static int powerOfTwoCeiling(int reference) int power = (int) Math.ceil(Math.log(reference) / Math.log(2d)); return (int) Math.pow(2d, power); } + + /** + * Returns the value that is the nearest power of 2 less than or equal to the given value. + * + * @param reference the reference value. The power of 2 returned is less than or equal to this value. + * + * @return the value that is the nearest power of 2 less than or equal to the reference value + */ + public static int powerOfTwoFloor(int reference) + { + int power = (int) Math.floor(Math.log(reference) / Math.log(2d)); + return (int) Math.pow(2d, power); + } + + /** + * Intersect a line with a convex polytope and return the intersection points. + *

    + * See "3-D Computer Graphics" by Samuel R. Buss, 2005, Section X.1.4. + * + * @param line the line to intersect with the polytope. + * @param planes the planes defining the polytope. Each plane's normal must point away from the the polytope, i.e. + * each plane's positive halfspace is outside the polytope. (Note: This is the opposite convention + * from that of a view frustum.) + * + * @return the points of intersection, or null if the line does not intersect the polytope. Two points are returned + * if the line both enters and exits the polytope. One point is retured if the line origin is within the + * polytope. + * + * @throws IllegalArgumentException if the line is null or ill-formed, the planes array is null or there are fewer + * than three planes. + */ + public static Intersection[] polytopeIntersect(Line line, Plane[] planes) + { + if (line == null) + { + String message = Logging.getMessage("nullValue.LineIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + // Algorithm from "3-D Computer Graphics" by Samuel R. Buss, 2005, Section X.1.4. + + // Determine intersection with each plane and categorize the intersections as "front" if the line intersects + // the front side of the plane (dot product of line direction with plane normal is negative) and "back" if the + // line intersects the back side of the plane (dot product of line direction with plane normal is positive). + + double fMax = -Double.MAX_VALUE; + double bMin = Double.MAX_VALUE; + boolean isTangent = false; + + Vec4 u = line.getDirection(); + Vec4 p = line.getOrigin(); + + for (Plane plane : planes) + { + Vec4 n = plane.getNormal(); + double d = -plane.getDistance(); + + double s = u.dot3(n); + if (s == 0) // line is parallel to plane + { + double pdn = p.dot3(n); + if (pdn > d) // is line in positive halfspace (in front of) of the plane? + return null; // no intersection + else + { + if (pdn == d) + isTangent = true; // line coincident with plane + continue; // line is in negative halfspace; possible intersection; check other planes + } + } + + // Determine whether front or back intersection. + double a = (d - p.dot3(n)) / s; + if (u.dot3(n) < 0) // line intersects front face and therefore entering polytope + { + if (a > fMax) + { + if (a > bMin) + return null; + fMax = a; + } + } + else // line intersects back face and therefore leaving polytope + { + if (a < bMin) + { + if (a < 0 || a < fMax) + return null; + bMin = a; + } + } + } + + // Compute the Cartesian intersection points. There will be no more than two. + if (fMax >= 0) // intersects frontface and backface; point origin is outside the polytope + return new Intersection[] + { + new Intersection(p.add3(u.multiply3(fMax)), isTangent), + new Intersection(p.add3(u.multiply3(bMin)), isTangent) + }; + else // intersects backface only; point origin is within the polytope + return new Intersection[] {new Intersection(p.add3(u.multiply3(bMin)), isTangent)}; + } + + /** + * Determines whether a {@link LatLon} location is located inside a given polygon. + * + * @param location the location + * @param locations the list of positions describing the polygon. Last one should be the same as the first one. + * + * @return true if the location is inside the polygon. + */ + public static boolean isLocationInside(LatLon location, Iterable locations) + { + if (location == null) + { + String message = Logging.getMessage("nullValue.LatLonIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + java.util.Iterator iter = locations.iterator(); + if (!iter.hasNext()) + { + return false; + } + + // Test for even/odd number of intersections with a constant latitude line going through the given location. + boolean result = false; + LatLon p1 = iter.next(); + while (iter.hasNext()) + { + LatLon p2 = iter.next(); + +// Developped for clarity +// double lat = location.getLatitude().degrees; +// double lon = location.getLongitude().degrees; +// double lat1 = p1.getLatitude().degrees; +// double lon1 = p1.getLongitude().degrees; +// double lat2 = p2.getLatitude().degrees; +// double lon2 = p2.getLongitude().degrees; +// if ( ((lat2 <= lat && lat < lat1) || (lat1 <= lat && lat < lat2)) +// && (lon < (lon1 - lon2) * (lat - lat2) / (lat1 - lat2) + lon2) ) +// result = !result; + + if (((p2.getLatitude().degrees <= location.getLatitude().degrees + && location.getLatitude().degrees < p1.getLatitude().degrees) || + (p1.getLatitude().degrees <= location.getLatitude().degrees + && location.getLatitude().degrees < p2.getLatitude().degrees)) + && (location.getLongitude().degrees < (p1.getLongitude().degrees - p2.getLongitude().degrees) + * (location.getLatitude().degrees - p2.getLatitude().degrees) + / (p1.getLatitude().degrees - p2.getLatitude().degrees) + p2.getLongitude().degrees)) + result = !result; + + p1 = p2; + } + return result; + } } diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/util/measure/AreaMeasurer.java b/WorldWindAndroid/src/gov/nasa/worldwind/util/measure/AreaMeasurer.java new file mode 100644 index 0000000..5b6204b --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/util/measure/AreaMeasurer.java @@ -0,0 +1,514 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ + +package gov.nasa.worldwind.util.measure; + +import gov.nasa.worldwind.geom.*; +import gov.nasa.worldwind.globes.Globe; +import gov.nasa.worldwind.util.GeometryBuilder; +import gov.nasa.worldwind.util.Logging; +import gov.nasa.worldwind.util.WWMath; + +import java.util.ArrayList; + +/** + * Utility class to compute approximations of projected and surface (terrain following) area on a globe. + * + *

    To properly compute surface area the measurer must be provided with a list of positions that describe a + * closed path - one which last position is equal to the first.

    + * + *

    Segments which are longer then the current maxSegmentLength will be subdivided along lines following the current + * pathType - {@link gov.nasa.worldwind.render.Polyline#LINEAR}, {@link gov.nasa.worldwind.render.Polyline#RHUMB_LINE} + * or {@link gov.nasa.worldwind.render.Polyline#GREAT_CIRCLE}.

    + * + *

    Projected or non terrain following area is computed in a sinusoidal projection which is equivalent or equal area. + * Surface or terrain following area is approximated by sampling the path bounding sector with square cells along a + * grid. Cells which center is inside the path have their area estimated and summed according to the overall slope + * at the cell south-west corner.

    + * + * @author Patrick Murris + * @version $Id: AreaMeasurer.java 1171 2013-02-11 21:45:02Z dcollins $ + * @see gov.nasa.worldwind.util.measure.MeasureTool + * @see LengthMeasurer + */ +public class AreaMeasurer extends LengthMeasurer implements MeasurableArea +{ + private static final double DEFAULT_AREA_SAMPLING_STEPS = 32; // sampling grid max rows or cols + + private ArrayList subdividedPositions; + private Cell[][] sectorCells; + private Double[][] sectorElevations; + private double areaTerrainSamplingSteps = DEFAULT_AREA_SAMPLING_STEPS; + protected double surfaceArea = -1; + protected double projectedArea = -1; + + public AreaMeasurer() + { + } + + public AreaMeasurer(ArrayList positions) + { + super(positions); + } + + protected void clearCachedValues() + { + super.clearCachedValues(); + this.subdividedPositions = null; + this.projectedArea = -1; + this.surfaceArea = -1; + } + + public void setPositions(ArrayList positions) + { + Sector oldSector = getBoundingSector(); + super.setPositions(positions); // will call clearCachedData() + + if (getBoundingSector() == null || !getBoundingSector().equals(oldSector)) + { + this.sectorCells = null; + this.sectorElevations = null; + } + } + + /** + * Get the sampling grid maximum number of rows or columns for terrain following surface area approximation. + * + * @return the sampling grid maximum number of rows or columns. + */ + public double getAreaTerrainSamplingSteps() + { + return this.areaTerrainSamplingSteps; + } + + /** + * Set the sampling grid maximum number of rows or columns for terrain following surface area approximation. + * + * @param steps the sampling grid maximum number of rows or columns. + * @throws IllegalArgumentException if steps is less then one. + */ + public void setAreaTerrainSamplingSteps(double steps) + { + if (steps < 1) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", steps); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (this.areaTerrainSamplingSteps != steps) + { + this.areaTerrainSamplingSteps = steps; + this.surfaceArea = -1; + this.projectedArea = -1; + // Invalidate cached data + this.sectorCells = null; + this.sectorElevations = null; + } + } + + /** + * Get the surface area approximation for the current path or shape. + * + *

    If the measurer is set to follow terrain, the computed area will account for terrain deformations. Otherwise + * the area is that of the path once projected at sea level - elevation zero.

    + * + * @param globe the globe to draw terrain information from. + * @return the current shape surface area or -1 if the position list does not describe a closed path or is too short. + * @throws IllegalArgumentException if globe is null. + */ + public double getArea(Globe globe) + { + return this.isFollowTerrain() ? getSurfaceArea(globe) : getProjectedArea(globe); + } + + public double getSurfaceArea(Globe globe) + { + if (globe == null) + { + String message = Logging.getMessage("nullValue.GlobeIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (this.surfaceArea < 0) + this.surfaceArea = this.computeSurfaceAreaSampling(globe, this.areaTerrainSamplingSteps); + + return this.surfaceArea; + } + + public double getProjectedArea(Globe globe) + { + throw new RuntimeException("Not implemented"); +// if (globe == null) +// { +// String message = Logging.getMessage("nullValue.GlobeIsNull"); +// Logging.error(message); +// throw new IllegalArgumentException(message); +// } +// +// if (this.projectedArea < 0) +// this.projectedArea = this.computeProjectedAreaGeometry(globe); +// +// return this.projectedArea; + } + + public double getPerimeter(Globe globe) + { + return getLength(globe); + } + + public double getWidth(Globe globe) + { + if (globe == null) + { + String message = Logging.getMessage("nullValue.GlobeIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + Sector sector = getBoundingSector(); + if (sector != null) + return globe.getRadiusAt(sector.getCentroid()) * sector.getDeltaLon().radians + * Math.cos(sector.getCentroid().getLatitude().radians); + + return -1; + } + + public double getHeight(Globe globe) + { + if (globe == null) + { + String message = Logging.getMessage("nullValue.GlobeIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + Sector sector = getBoundingSector(); + if (sector != null) + return globe.getRadiusAt(sector.getCentroid()) * sector.getDeltaLat().radians; + + return -1; + } + + // *** Computing area ****************************************************************** + + protected class Cell + { + Sector sector; + double projectedArea, surfaceArea; + + public Cell(Sector sector, double projected, double surface) + { + this.sector = sector; + this.projectedArea = projected; + this.surfaceArea = surface; + } + } + + // *** Projected area *** + + // Tessellate the path in lat-lon space, then sum each triangle area. +// protected double computeProjectedAreaGeometry(Globe globe) +// { +// Sector sector = getBoundingSector(); +// if (sector != null && this.isClosedShape()) +// { +// // Subdivide long segments if needed +// if (this.subdividedPositions == null) +// this.subdividedPositions = subdividePositions(globe, getPositions(), getMaxSegmentLength() +// , isFollowTerrain(), getPathType()); +// // First: tessellate polygon +// int verticesCount = this.subdividedPositions.size() - 1; // trim last pos which is same as first +// float[] verts = new float[verticesCount * 3]; +// // Prepare vertices +// int idx = 0; +// for (int i = 0; i < verticesCount; i++) +// { +// // Vertices coordinates are x=lon y=lat in radians, z = elevation zero +// verts[idx++] = (float)this.subdividedPositions.get(i).getLongitude().radians; +// verts[idx++] = (float)this.subdividedPositions.get(i).getLatitude().radians; +// verts[idx++] = 0f; +// } +// // Tessellate +// GeometryBuilder gb = new GeometryBuilder(); +// GeometryBuilder.IndexedTriangleArray ita = gb.tessellatePolygon2(0, verticesCount, verts); +// // Second: sum triangles area +// double area = 0; +// int[] indices = ita.getIndices(); +// int triangleCount = ita.getIndexCount() / 3; +// for (int i = 0; i < triangleCount; i++) +// { +// idx = i * 3; +// area += computeTriangleProjectedArea(globe, ita.getVertices(), indices[idx] * 3 +// , indices[idx + 1] * 3, indices[idx + 2] * 3); +// } +// return area; +// } +// return -1; +// } + + // Compute triangle area in a sinusoidal projection centered at the triangle center. + // Note sinusoidal projection is equivalent or equal erea. + protected double computeTriangleProjectedArea(Globe globe, float[] verts, int a, int b, int c) + { + // http://www.mathopenref.com/coordtrianglearea.html + double area = Math.abs(verts[a] * (verts[b + 1] - verts[c + 1]) + + verts[b] * (verts[c + 1] - verts[a + 1]) + + verts[c] * (verts[a + 1] - verts[b + 1])) / 2; // square radians + // Compute triangle center + double centerLat = (verts[a + 1] + verts[b + 1] + verts[c + 1]) / 3; + double centerLon = (verts[a] + verts[b] + verts[c]) / 3; + // Apply globe radius at triangle center and scale down area according to center latitude cosine + double radius = globe.getRadiusAt(Angle.fromRadians(centerLat), Angle.fromRadians(centerLon)); + area *= Math.cos(centerLat) * radius * radius; // Square meter + + return area; + } + + // *** Surface area - terrain following *** + + // Sample the path bounding sector with square cells which area are approximated according to the surface normal at + // the cell south-west corner. + protected double computeSurfaceAreaSampling(Globe globe, double steps) + { + Sector sector = getBoundingSector(); + if (sector != null && this.isClosedShape()) + { + // Subdivide long segments if needed + if (this.subdividedPositions == null) + this.subdividedPositions = subdividePositions(globe, getPositions(), getMaxSegmentLength(), + true, getPathType()); + + // Sample the bounding sector with cells about the same length in side - squares + double stepRadians = Math.max(sector.getDeltaLatRadians() / steps, sector.getDeltaLonRadians() / steps); + int latSteps = (int)Math.round(sector.getDeltaLatRadians() / stepRadians); + int lonSteps = (int)Math.round(sector.getDeltaLonRadians() / stepRadians + * Math.cos(sector.getCentroid().getLatitude().radians)); + double latStepRadians = sector.getDeltaLatRadians() / latSteps; + double lonStepRadians = sector.getDeltaLonRadians() / lonSteps; + + if (this.sectorCells == null) + this.sectorCells = new Cell[latSteps][lonSteps]; + if (this.sectorElevations == null) + this.sectorElevations = new Double[latSteps + 1][lonSteps + 1]; + + double area = 0; + for (int i = 0; i < latSteps; i++) + { + double lat = sector.minLatitude.radians + latStepRadians * i; + // Compute this latitude row cells area + double radius = globe.getRadiusAt(Angle.fromRadians(lat + latStepRadians / 2), + sector.getCentroid().getLongitude()); + double cellWidth = lonStepRadians * radius * Math.cos(lat + latStepRadians / 2); + double cellHeight = latStepRadians * radius; + double cellArea = cellWidth * cellHeight; + + for (int j = 0; j < lonSteps; j++) + { + double lon = sector.minLongitude.radians + lonStepRadians * j; + Sector cellSector = Sector.fromRadians(lat, lat + latStepRadians, lon, lon + lonStepRadians); + // Select cells which center is inside the shape + if (WWMath.isLocationInside(cellSector.getCentroid(), this.subdividedPositions)) + { + Cell cell = this.sectorCells[i][j]; + if (cell == null || cell.surfaceArea == -1) + { + // Compute suface area using terrain normal in SW corner + // Corners elevation + double eleSW = sectorElevations[i][j] != null ? sectorElevations[i][j] + : globe.getElevation(Angle.fromRadians(lat), Angle.fromRadians(lon)); + double eleSE = sectorElevations[i][j + 1] != null ? sectorElevations[i][j + 1] + : globe.getElevation(Angle.fromRadians(lat), Angle.fromRadians(lon + lonStepRadians)); + double eleNW = sectorElevations[i + 1][j] != null ? sectorElevations[i + 1][j] + : globe.getElevation(Angle.fromRadians(lat + latStepRadians), Angle.fromRadians(lon)); + // Cache elevations + sectorElevations[i][j] = eleSW; + sectorElevations[i][j + 1] = eleSE; + sectorElevations[i + 1][j] = eleNW; + // Compute normal + Vec4 vx = new Vec4(cellWidth, 0, eleSE - eleSW).normalize3(); + Vec4 vy = new Vec4(0, cellHeight, eleNW - eleSW).normalize3(); + Vec4 normalSW = vx.cross3(vy).normalize3(); // point toward positive Z + // Compute slope factor + double tan = Math.tan(Vec4.UNIT_Z.angleBetween3(normalSW).radians); + double slopeFactor = Math.sqrt(1 + tan * tan); + // Create and cache cell + cell = new Cell(cellSector, cellArea, cellArea * slopeFactor); + this.sectorCells[i][j] = cell; + } + // Add cell area + area += cell.surfaceArea; + } + } + } + return area; + } + return -1; + } + +// Below code is an attempt at computing the surface area using geometry. + +// private static final double DEFAULT_AREA_CONVERGENCE_PERCENT = 2; // stop sudividing when increase in area + // is less then this percent +// private double areaTerrainConvergencePercent = DEFAULT_AREA_CONVERGENCE_PERCENT; + +// private int triangleCount = 0; +// // Tessellate the path in lat-lon space, then sum each triangle surface area. +// protected double computeSurfaceAreaGeometry(Globe globe) +// { +// long t0 = System.nanoTime(); +// this.triangleCount = 0; +// Sector sector = getBoundingSector(); +// if (sector != null && this.isClosedShape()) +// { +// // Subdivide long segments if needed +// if (this.subdividedPositions == null) +// this.subdividedPositions = subdividePositions(globe, getPositions(), getMaxSegmentLength() +// , isFollowTerrain(), getPathType()); +// // First: tessellate polygon +// int verticesCount = this.subdividedPositions.size() - 1; // trim last pos which is same as first +// float[] verts = new float[verticesCount * 3]; +// // Prepare vertices +// int idx = 0; +// for (int i = 0; i < verticesCount; i++) +// { +// // Vertices coordinates are x=lon y=lat in radians, z = elevation zero +// verts[idx++] = (float)this.subdividedPositions.get(i).getLongitude().radians; +// verts[idx++] = (float)this.subdividedPositions.get(i).getLatitude().radians; +// verts[idx++] = 0f; +// } +// // Tessellate +// GeometryBuilder gb = new GeometryBuilder(); +// GeometryBuilder.IndexedTriangleArray ita = gb.tessellatePolygon2(0, verticesCount, verts); +// // Second: sum triangles area +// double area = 0; +// int triangleCount = ita.getIndexCount() / 3; +// for (int i = 0; i < triangleCount; i++) +// { +// idx = i * 3; +// area += computeIndexedTriangleSurfaceArea(globe, ita, idx); +// } +// long t1 = System.nanoTime(); +// System.out.println("Surface area geometry: " + area + " - " + (t1 - t0) / 1e3 + " micro sec for " + this.triangleCount + " triangles"); +// return area; +// } +// return -1; +// } +// +// private double computeIndexedTriangleSurfaceArea(Globe globe, GeometryBuilder.IndexedTriangleArray ita, int idx) +// { +// // Create a one triangle indexed array +// GeometryBuilder gb = new GeometryBuilder(); +// int[] indices = new int[] {0, 1, 2}; +// float[] vertices = new float[9]; +// System.arraycopy(ita.getVertices(), ita.getIndices()[idx] * 3, vertices, 0, 3); +// System.arraycopy(ita.getVertices(), ita.getIndices()[idx + 1] * 3, vertices, 3, 3); +// System.arraycopy(ita.getVertices(), ita.getIndices()[idx + 2] * 3, vertices, 6, 3); +// GeometryBuilder.IndexedTriangleArray triangleIta = new GeometryBuilder.IndexedTriangleArray(3, indices, 3, vertices); +// +// // Get triangle area +// double area = computeIndexedTriangleArraySurfaceArea(globe, triangleIta); +// if (area < 10) +// { +// // Do not subdivide below some area +// this.triangleCount++; +// return area; +// } +// +// // Subdivide and get area again. If increase is larger then some percentage, recurse on each of four triangles +// gb.subdivideIndexedTriangleArray(triangleIta); +// double subArea = computeIndexedTriangleArraySurfaceArea(globe, triangleIta); +// double delta = subArea - area; +// +// // *** Debug *** +// System.out.println((delta > 1 && delta > area * this.areaTerrainConvergencePercent / 100 ? "more" : "OK") +// + " Delta: " + delta + ", area: " + area + ", sub area: " + subArea); +// +// if (delta > 1 && delta > area * this.areaTerrainConvergencePercent / 100) +// { +// // Recurse on four sub triangles +// subArea = 0; +// for (int i = 0; i < 4; i++) +// subArea += computeIndexedTriangleSurfaceArea(globe, triangleIta, i * 3); +// } +// else +// this.triangleCount += 4; +// +// return subArea; +// } + +// private double computeIndexedTriangleSurfaceAreaIteration(Globe globe, GeometryBuilder.IndexedTriangleArray ita, int idx) +// { +// // Create a one triangle indexed array +// GeometryBuilder gb = new GeometryBuilder(); +// int[] indices = new int[] {0, 1, 2}; +// float[] vertices = new float[9]; +// System.arraycopy(ita.getVertices(), ita.getIndices()[idx] * 3, vertices, 0, 3); +// System.arraycopy(ita.getVertices(), ita.getIndices()[idx + 1] * 3, vertices, 3, 3); +// System.arraycopy(ita.getVertices(), ita.getIndices()[idx + 2] * 3, vertices, 6, 3); +// GeometryBuilder.IndexedTriangleArray triangleIta = new GeometryBuilder.IndexedTriangleArray(3, indices, 3, vertices); +// +// // Get triangle area +// double area = computeIndexedTriangleArraySurfaceArea(globe, triangleIta); +// +// // Subdivide and get area again until increase is smaller then some percentage +// double delta = Double.MAX_VALUE; +// while (delta > (area - delta) * this.areaTerrainConvergencePercent / 100) +// { +// gb.subdivideIndexedTriangleArray(triangleIta); +// double subArea = computeIndexedTriangleArraySurfaceArea(globe, triangleIta); +// delta = subArea - area; +// area = subArea; +// } +// System.out.println("Triangle " + idx / 3 + " tot triangles: " + triangleIta.getIndexCount() / 3); +// return area; +// } + +// private double computeIndexedTriangleArraySurfaceArea(Globe globe, GeometryBuilder.IndexedTriangleArray ita) +// { +// int a, b, c; +// double area = 0; +// for (int i = 0; i < ita.getIndexCount(); i += 3) +// { +// // Sum each triangle area +// a = ita.getIndices()[i] * 3; +// b = ita.getIndices()[i + 1] * 3; +// c = ita.getIndices()[i + 2] * 3; +// area += computeTriangleSurfaceArea(globe, ita.getVertices(), a, b, c); +// } +// return area; +// } +// +// protected double computeTriangleSurfaceArea(Globe globe, float[] verts, int a, int b, int c) +// { +// // Triangle surface area is half the cross product length of any two edges +// Vec4 pa = getSurfacePointSinusoidal(globe, verts[a + 1], verts[a]); +// Vec4 pb = getSurfacePointSinusoidal(globe, verts[b + 1], verts[b]); +// Vec4 pc = getSurfacePointSinusoidal(globe, verts[c + 1], verts[c]); +// Vec4 AB = pb.subtract3(pa); +// Vec4 AC = pc.subtract3(pa); +// return 0.5 * AB.cross3(AC).getLength3(); +// } + +// protected Vec4 getSurfacePoint(Globe globe, float latRadians, float lonRadians) +// { +// Angle latitude = Angle.fromRadians(latRadians); +// Angle longitude = Angle.fromRadians(lonRadians); +// return globe.computePointFromPosition(latitude, longitude, globe.getElevation(latitude, longitude)); +// } + +// protected Vec4 getSurfacePointSinusoidal(Globe globe, float latRadians, float lonRadians) +// { +// Angle latitude = Angle.fromRadians(latRadians); +// Angle longitude = Angle.fromRadians(lonRadians); +// double radius = globe.getRadiusAt(latitude, longitude); +// return new Vec4(radius * lonRadians * latitude.cos(), radius * latRadians, +// globe.getElevation(latitude, longitude)); +// } + + +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/util/measure/LengthMeasurer.java b/WorldWindAndroid/src/gov/nasa/worldwind/util/measure/LengthMeasurer.java new file mode 100644 index 0000000..b8f3f79 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/util/measure/LengthMeasurer.java @@ -0,0 +1,418 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ + +package gov.nasa.worldwind.util.measure; + +import gov.nasa.worldwind.avlist.AVKey; +import gov.nasa.worldwind.geom.*; +import gov.nasa.worldwind.globes.Globe; +import gov.nasa.worldwind.util.Logging; + +import java.util.ArrayList; + +/** + * Utility class to measure length along a path on a globe. + * + *

    The measurer must be provided a list of at least two positions to be able to compute a distance.

    + * + *

    Segments which are longer then the current maxSegmentLength will be subdivided along lines following the current + * pathType - Polyline.LINEAR, Polyline.RHUMB_LINE or Polyline.GREAT_CIRCLE.

    + * + *

    If the measurer is set to follow terrain, the computed length will account for terrain deformations as + * if someone was walking along that path. Otherwise the length is the sum of the cartesian distance between + * each positions.

    + * + *

    When following terrain the measurer will sample terrain elevations at regular intervals along the path. The + * minimum number of samples used for the whole length can be set with setLengthTerrainSamplingSteps(). However, + * the sampling process will keep a minimum interval of 30 meters between samples. + * + * @author Patrick Murris + * @version $Id: LengthMeasurer.java 1171 2013-02-11 21:45:02Z dcollins $ + * @see gov.nasa.worldwind.util.measure.MeasureTool + */ +public class LengthMeasurer implements MeasurableLength +{ + private static final double DEFAULT_TERRAIN_SAMPLING_STEPS = 128; // number of samples when following terrain + private static final double DEFAULT_MAX_SEGMENT_LENGTH = 100e3; // size above which segments are subdivided + private static final double DEFAULT_MIN_SEGMENT_LENGTH = 30; // minimum length of a terrain following subdivision + + private ArrayList positions; + private ArrayList subdividedPositions; + private boolean followTerrain = false; + private String pathType = AVKey.GREAT_CIRCLE; + private double maxSegmentLength = DEFAULT_MAX_SEGMENT_LENGTH; + private Sector sector; + private double lengthTerrainSamplingSteps = DEFAULT_TERRAIN_SAMPLING_STEPS; + protected double length = -1; + + public LengthMeasurer() + { + } + + public LengthMeasurer(ArrayList positions) + { + this.setPositions(positions); + } + + protected void clearCachedValues() + { + this.subdividedPositions = null; + this.length = -1; + } + + public ArrayList getPositions() + { + return this.positions; + } + + public void setPositions(ArrayList positions, double elevation) + { + if (positions == null) + { + String message = Logging.getMessage("nullValue.PositionsListIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + ArrayList newPositions = new ArrayList(); + for (LatLon pos : positions) + newPositions.add(new Position(pos, elevation)); + + setPositions(newPositions); + } + + public void setPositions(ArrayList positions) + { + if (positions == null) + { + String message = Logging.getMessage("nullValue.PositionsListIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.positions = positions; + if (this.positions.size() > 2) + this.sector = Sector.boundingSector(this.positions); + else + this.sector = null; + + clearCachedValues(); + } + + public boolean isFollowTerrain() + { + return this.followTerrain; + } + + /** + * Set whether measurements should account for terrain deformations. + * + * @param followTerrain set to true if measurements should account for terrain deformations. + */ + public void setFollowTerrain(boolean followTerrain) + { + if (this.followTerrain != followTerrain) + { + this.followTerrain = followTerrain; + clearCachedValues(); + } + } + + public String getPathType() + { + return this.pathType; + } + + /** + * Sets the type of path used when subdividing long segments, one of AVKey.GREAT_CIRCLE, which draws segments + * as a great circle, AVKey.LINEAR, which determines the intermediate positions between segments by + * interpolating the segment endpoints, or AVKey.RHUMB_LINE, which draws segments as a line of constant heading. + * + * @param pathType the type of path to measure. + */ + public void setPathType(String pathType) + { + if (this.pathType != pathType) + { + this.pathType = pathType; + clearCachedValues(); + } + } + + /** + * Get the maximum length a segment can have before being subdivided along a line following the current pathType. + * + * @return the maximum length a segment can have before being subdivided. + */ + public double getMaxSegmentLength() + { + return this.maxSegmentLength; + } + + /** + * Set the maximum length a segment can have before being subdivided along a line following the current pathType. + * + * @param length the maximum length a segment can have before being subdivided. + */ + public void setMaxSegmentLength(double length) + { + if (length <= 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", length); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (this.maxSegmentLength != length) + { + this.maxSegmentLength = length; + clearCachedValues(); + } + } + + public Sector getBoundingSector() + { + if (this.sector == null && this.positions != null && this.positions.size() > 2) + this.sector = Sector.boundingSector(this.positions); + + return this.sector; + } + + /** + * Returns true if the current position list describe a closed path - one which last position is equal + * to the first. + * + * @return true if the current position list describe a closed path. + */ + public boolean isClosedShape() + { + return this.positions != null + && this.positions.size() > 1 + && this.positions.get(0).equals(this.positions.get(this.positions.size() - 1)); + } + + /** + * Get the number of terrain elevation samples used along the path to approximate it's terrain following length. + * + * @return the number of terrain elevation samples used. + */ + public double getLengthTerrainSamplingSteps() + { + return this.lengthTerrainSamplingSteps; + } + + /** + * Set the number of terrain elevation samples to be used along the path to approximate it's terrain following + * length. + * + * @param steps the number of terrain elevation samples to be used. + */ + public void setLengthTerrainSamplingSteps(double steps) + { + if (steps < 1) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", steps); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (this.lengthTerrainSamplingSteps != steps) + { + this.lengthTerrainSamplingSteps = steps; + this.subdividedPositions = null; + this.length = -1; + } + } + + /** + * Get the path length in meter. + * + *

    If the measurer is set to follow terrain, the computed length will account for terrain deformations as + * if someone was walking along that path. Otherwise the length is the sum of the cartesian distance between + * each positions.

    + * + * @param globe the globe to draw terrain information from. + * @return the current path length or -1 if the position list is too short. + */ + public double getLength(Globe globe) + { + if (globe == null) + { + String message = Logging.getMessage("nullValue.GlobeIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + if (this.length < 0) + this.length = this.computeLength(globe, this.followTerrain); + + return this.length; + } + + // *** Computing length ***************************************************************************** + + protected double computeLength(Globe globe, boolean followTerrain) + { + if (this.positions == null || this.positions.size() < 2) + return -1; + + if (this.subdividedPositions == null) + { + // Subdivide path so as to have at least segments smaller then maxSegmentLenth. If follow terrain, + // subdivide so as to have at least lengthTerrainSamplingSteps segments, but no segments shorter then + // DEFAULT_MIN_SEGMENT_LENGTH either. + double maxLength = this.maxSegmentLength; + if (followTerrain) + { + // Recurse to compute overall path length not following terrain + double pathLength = computeLength(globe, !followTerrain); + // Determine segment length to have enough sampling points + maxLength = pathLength / this.lengthTerrainSamplingSteps; + maxLength = Math.min(Math.max(maxLength, DEFAULT_MIN_SEGMENT_LENGTH), getMaxSegmentLength()); + } + this.subdividedPositions = subdividePositions(globe, this.positions, maxLength, + followTerrain, this.pathType); + } + + // Sum each segment length + double length = 0; + Vec4 p1 = globe.computePointFromPosition(this.subdividedPositions.get(0)); + for (int i = 1; i < subdividedPositions.size(); i++) + { + Vec4 p2 = globe.computePointFromPosition(this.subdividedPositions.get(i)); + length += p1.distanceTo3(p2); + p1 = p2; + } + return length; + } + +// // This tries not to use the globe.computePointFromPosition so as to report accurate length on flat globes +// // However, it shows significant deviations for east-west measurements. +// private double computeSegmentLengthLinearApprox(Globe globe, Position pos1, Position pos2) +// { +// // Cartesian distance approximation for short segments - only needs globe radius +// LatLon midPoint = LatLon.interpolate(.5, pos1, pos2); +// double radius = globe.getRadiusAt(midPoint); +// double cosLat = Math.cos(midPoint.getLatitude().radians); +// return new Vec4( +// (pos2.getLongitude().radians - pos1.getLongitude().radians) * radius * cosLat, +// (pos2.getLatitude().radians - pos1.getLatitude().radians) * radius, +// pos2.getElevation() - pos1.getElevation() +// ).getLength3(); // Meters +// } + + /** + * Subdivide a list of positions so that no segment is longer then the provided maxLength. + * + *

    If needed, new intermediate positions will be created along lines that follow the given pathType - one of + * Polyline.LINEAR, Polyline.RHUMB_LINE or Polyline.GREAT_CIRCLE. All position elevations will be either at the + * terrain surface if followTerrain is true, or interpolated according to the original elevations.

    + * + * @param globe the globe to draw elevations and points from. + * @param positions the original position list + * @param maxLength the maximum length for one segment. + * @param followTerrain true if the positions should be on the terrain surface. + * @param pathType the type of path to use in between two positions. + * @return a list of positions with no segment longer then maxLength and elevations following terrain or not. + */ + protected static ArrayList subdividePositions(Globe globe, ArrayList positions, + double maxLength, boolean followTerrain, String pathType) + { + return subdividePositions(globe, positions, maxLength, followTerrain, pathType, 0, positions.size()); + } + + /** + * Subdivide a list of positions so that no segment is longer then the provided maxLength. Only the positions + * between start and start + count - 1 will be processed. + * + *

    If needed, new intermediate positions will be created along lines that follow the given pathType - one of + * Polyline.LINEAR, Polyline.RHUMB_LINE or Polyline.GREAT_CIRCLE. All position elevations will be either at the + * terrain surface if followTerrain is true, or interpolated according to the original elevations.

    + * + * @param globe the globe to draw elevations and points from. + * @param positions the original position list + * @param maxLength the maximum length for one segment. + * @param followTerrain true if the positions should be on the terrain surface. + * @param pathType the type of path to use in between two positions. + * @param start the first position indice in the original list. + * @param count how many positions from the original list have to be processed and returned. + * @return a list of positions with no segment longer then maxLength and elevations following terrain or not. + */ + protected static ArrayList subdividePositions(Globe globe, ArrayList positions, + double maxLength, boolean followTerrain, String pathType, + int start, int count) + { + if (positions == null || positions.size() < start + count) + return positions; + + ArrayList newPositions = new ArrayList(); + // Add first position + Position pos1 = positions.get(start); + if (followTerrain) + newPositions.add(new Position(pos1, globe.getElevation(pos1.getLatitude(), pos1.getLongitude()))); + else + newPositions.add(pos1); + for(int i = 1; i < count; i++) + { + Position pos2 = positions.get(start + i); + double arcLengthRadians = LatLon.greatCircleDistance(pos1, pos2).radians; + double arcLength = arcLengthRadians * globe.getRadiusAt(LatLon.interpolate(.5, pos1, pos2)); + if (arcLength > maxLength) + { + // if necessary subdivide segment at regular intervals smaller then maxLength + Angle segmentAzimuth = null; + Angle segmentDistance = null; + int steps = (int)Math.ceil(arcLength / maxLength); // number of intervals - at least two + for (int j = 1; j < steps; j++) + { + float s = (float)j / steps; + LatLon destLatLon; + if (pathType.equals(AVKey.LINEAR)) + { + destLatLon = LatLon.interpolate(s, pos1, pos2); + } + else if (pathType.equals(AVKey.RHUMB_LINE)) + { + if (segmentAzimuth == null) + { + segmentAzimuth = LatLon.rhumbAzimuth(pos1, pos2); + segmentDistance = LatLon.rhumbDistance(pos1, pos2); + } + destLatLon = LatLon.rhumbEndPosition(pos1, segmentAzimuth.radians, + s * segmentDistance.radians); + } + else // GREAT_CIRCLE + { + if (segmentAzimuth == null) + { + segmentAzimuth = LatLon.greatCircleAzimuth(pos1, pos2); + segmentDistance = LatLon.greatCircleDistance(pos1, pos2); + } + destLatLon = LatLon.greatCircleEndPosition(pos1, segmentAzimuth.radians, + s * segmentDistance.radians); + } + // Set elevation + double elevation; + if (followTerrain) + elevation = globe.getElevation(destLatLon.getLatitude(), destLatLon.getLongitude()); + else + elevation = pos1.elevation * (1 - s) + pos2.elevation * s; + // Add new position + newPositions.add(new Position(destLatLon, elevation)); + } + } + // Finally add the segment end position + if (followTerrain) + newPositions.add(new Position(pos2, globe.getElevation(pos2.getLatitude(), pos2.getLongitude()))); + else + newPositions.add(pos2); + // Prepare for next segment + pos1 = pos2; + } + return newPositions; + } + +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/util/pkm/ETC1Compressor.java b/WorldWindAndroid/src/gov/nasa/worldwind/util/pkm/ETC1Compressor.java new file mode 100644 index 0000000..61e5e8b --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/util/pkm/ETC1Compressor.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ + +package gov.nasa.worldwind.util.pkm; + +import android.graphics.*; +import android.opengl.ETC1Util; +import android.opengl.ETC1Util.ETC1Texture; +import gov.nasa.worldwind.WorldWindowImpl; +import gov.nasa.worldwind.util.Logging; +import gov.nasa.worldwind.util.WWIO; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Compress ETC1 Images + * + * Created by kedzie on 4/14/14. + */ +public class ETC1Compressor { + + /** + * Compress Bitmap to ETC1 Texture without alpha channel + * @param buffer buffer containing the image to compress, alpha channel is discarded + * @return array containing color texture + */ + public static ETC1Texture[] compressImageBuffer(ByteBuffer buffer) { + return compressImageBuffer(buffer, false); + } + + /** + * Compress Bitmap to ETC1 Texture, optionally with alpha channel in seperate texture + * @param buffer buffer containing the image to compress, with alpha channel (if applicable) + * @param extractAlpha whether to extract alpha channel into seperate texture + * @return array containing color texture and alpha texture, if extracted alphaMap + */ + public static ETC1Texture[] compressImageBuffer(ByteBuffer buffer, boolean extractAlpha) { + return compressImage(BitmapFactory.decodeStream(WWIO.getInputStreamFromByteBuffer(buffer)), extractAlpha); + } + + /** + * Compress Bitmap to ETC1 Texture without alpha channel + * @param image the image to compress, alpha channel is discarded + * @return array containing color texture + */ + public static ETC1Texture[] compressImage(Bitmap image) { + return compressImage(image, false); + } + + /** + * Compress Bitmap to ETC1 Texture, optionally with alpha channel in seperate texture + * @param image the image to compress, with alpha channel (if applicable) + * @param extractAlpha whether to extract alpha channel into seperate texture + * @return array containing color texture and alpha texture, if extracted alphaMap + */ + public static ETC1Texture[] compressImage(Bitmap image, boolean extractAlpha) { + if(WorldWindowImpl.DEBUG) + Logging.verbose("Compressing ETC1 texture "); + ETC1Texture color = compressBitmap565(convert(image, Bitmap.Config.RGB_565)); + + if(extractAlpha && image.hasAlpha()) { + if(WorldWindowImpl.DEBUG) + Logging.verbose("Extracting ETC1 alpha texture.."); + Bitmap alphaMap = extractAlpha(image); + ETC1Texture alphaTex = compressBitmap565(alphaMap); + return new ETC1Texture[] { color, alphaTex }; + } else { + return new ETC1Texture[]{color}; + } + } + + /** + * Compress RGB_565 bitmap + * @param bitmap565 Bitmap in RGB_565 format + * @return compressed texture + */ + private static ETC1Texture compressBitmap565(Bitmap bitmap565) { + if(!bitmap565.getConfig().equals(Bitmap.Config.RGB_565)) + throw new IllegalArgumentException("Bitmap must be RGB_565"); + + final int width = bitmap565.getWidth(); + final int height = bitmap565.getHeight(); + int size = bitmap565.getRowBytes() * height; + ByteBuffer bb = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()); + bitmap565.copyPixelsToBuffer(bb); + bb.position(0); + int pixelBytes = 2; + return ETC1Util.compressTexture(bb, width, height, pixelBytes, pixelBytes * width); + } + + public static Bitmap convert(Bitmap input, Bitmap.Config format) { + Bitmap output = Bitmap.createBitmap( input.getWidth(), input.getHeight(), format ); + Canvas c = new Canvas(output); + Paint p = new Paint(); + p.setFilterBitmap(true); + p.setDither(true); + c.drawBitmap(input,0,0,p); + input.recycle(); + return output; + } + + protected static Bitmap extractAlpha(Bitmap input) { + final int width = input.getWidth(); + final int height = input.getHeight(); + + Bitmap alphaMap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565); + + int[] pixels = new int[width*height]; + input.getPixels(pixels, 0, width, 0, 0, width, height); + for(int i=0; i 0) + { + newLatitude = limits.maxLatitude; + } + + if (longitude.compareTo(limits.minLongitude) < 0) + { + newLongitude = limits.minLongitude; + } + else if (longitude.compareTo(limits.maxLongitude) > 0) + { + newLongitude = limits.maxLongitude; + } + + return new LatLon(newLatitude, newLongitude); + } + + public static double limitCenterElevation(double value, OrbitViewLimits viewLimits) + { + if (viewLimits == null) + { + String message = Logging.getMessage("nullValue.ViewLimitsIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + double[] limits = viewLimits.getCenterElevationLimits(); + double newValue = value; + + if (value < limits[0]) + { + newValue = limits[0]; + } + else if (value > limits[1]) + { + newValue = limits[1]; + } + + return newValue; + } + + + + public static double limitZoom(double value, OrbitViewLimits viewLimits) + { + if (viewLimits == null) + { + String message = Logging.getMessage("nullValue.ViewLimitsIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + double[] limits = viewLimits.getZoomLimits(); + double newValue = value; + + if (value < limits[0]) + { + newValue = limits[0]; + } + else if (value > limits[1]) + { + newValue = limits[1]; + } + + return newValue; + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/view/BasicViewPropertyLimits.java b/WorldWindAndroid/src/gov/nasa/worldwind/view/BasicViewPropertyLimits.java new file mode 100644 index 0000000..26518bc --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/view/BasicViewPropertyLimits.java @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.view; + +import gov.nasa.worldwind.geom.Angle; +import gov.nasa.worldwind.geom.LatLon; +import gov.nasa.worldwind.geom.Sector; +import gov.nasa.worldwind.util.Logging; + +/** + * @author jym + * @version $Id$ + */ +public class BasicViewPropertyLimits implements ViewPropertyLimits +{ + + protected Sector eyeLocationLimits; + protected Angle minHeading; + protected Angle maxHeading; + protected Angle minPitch; + protected Angle maxPitch; + protected Angle minRoll; + protected Angle maxRoll; + protected double minEyeElevation; + protected double maxEyeElevation; + + public BasicViewPropertyLimits() + { + this.eyeLocationLimits = Sector.fromFullSphere(); + this.minEyeElevation = -Double.MAX_VALUE; + this.maxEyeElevation = Double.MAX_VALUE; + this.minHeading = Angle.NEG180; + this.maxHeading = Angle.POS180; + this.minPitch = Angle.ZERO; + this.maxPitch = Angle.POS90; + this.minRoll = Angle.NEG180; + this.maxRoll = Angle.POS180; + } + + public Sector getEyeLocationLimits() + { + return this.eyeLocationLimits; + } + + public void setEyeLocationLimits(Sector sector) + { + if (sector == null) + { + String message = Logging.getMessage("nullValue.SectorIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.eyeLocationLimits = sector; + } + + public double[] getEyeElevationLimits() + { + return new double[] {this.minEyeElevation, this.maxEyeElevation}; + } + + public void setEyeElevationLimits(double minValue, double maxValue) + { + this.minEyeElevation = minValue; + this.maxEyeElevation = maxValue; + } + + public Angle[] getHeadingLimits() + { + return new Angle[] {this.minHeading, this.maxHeading}; + } + + public void setHeadingLimits(Angle minAngle, Angle maxAngle) + { + if (minAngle == null || maxAngle == null) + { + String message = Logging.getMessage("nullValue.MinOrMaxAngleIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.minHeading = minAngle; + this.maxHeading = maxAngle; + } + + public Angle[] getPitchLimits() + { + return new Angle[] {this.minPitch, this.maxPitch}; + } + + public void setPitchLimits(Angle minAngle, Angle maxAngle) + { + if (minAngle == null || maxAngle == null) + { + String message = Logging.getMessage("nullValue.MinOrMaxAngleIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.minPitch = minAngle; + this.maxPitch = maxAngle; + } + + /** + * Get the limits for roll. + * + * @return The roll limits as a two element array {minRoll, maxRoll}, + */ + public Angle[] getRollLimits() + { + return new Angle[] { this.minRoll, this.maxRoll }; + } + + /** + * Set the roll limits. + * + * @param minAngle The smallest allowable roll. + * @param maxAngle The largest allowable roll. + */ + public void setRollLimits(Angle minAngle, Angle maxAngle) + { + if (minAngle == null || maxAngle == null) + { + String message = Logging.getMessage("nullValue.MinOrMaxAngleIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.minRoll = minAngle; + this.maxRoll = maxAngle; + } + + public static Angle limitHeading(Angle angle, ViewPropertyLimits viewLimits) + { + if (angle == null) + { + String message = Logging.getMessage("nullValue.AngleIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (viewLimits == null) + { + String message = Logging.getMessage("nullValue.ViewLimitsIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + Angle[] limits = viewLimits.getHeadingLimits(); + Angle newAngle = angle; + + if (angle.compareTo(limits[0]) < 0) + { + newAngle = limits[0]; + } + else if (angle.compareTo(limits[1]) > 0) + { + newAngle = limits[1]; + } + + return newAngle; + } + + public static Angle limitPitch(Angle angle, ViewPropertyLimits viewLimits) + { + if (angle == null) + { + String message = Logging.getMessage("nullValue.AngleIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (viewLimits == null) + { + String message = Logging.getMessage("nullValue.ViewLimitsIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + Angle[] limits = viewLimits.getPitchLimits(); + Angle newAngle = angle; + if (angle.compareTo(limits[0]) < 0) + { + newAngle = limits[0]; + } + else if (angle.compareTo(limits[1]) > 0) + { + newAngle = limits[1]; + } + + return newAngle; + } + + /** + * Clamp a roll angle to the range specified in a limit object. + * + * @param angle Angle to clamp to the allowed range. + * @param viewLimits defines the roll limits. + */ + public static Angle limitRoll(Angle angle, ViewPropertyLimits viewLimits) + { + if (angle == null) + { + String message = Logging.getMessage("nullValue.AngleIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (viewLimits == null) + { + String message = Logging.getMessage("nullValue.ViewLimitsIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + Angle[] limits = viewLimits.getRollLimits(); + Angle newAngle = angle; + if (angle.compareTo(limits[0]) < 0) + { + newAngle = limits[0]; + } + else if (angle.compareTo(limits[1]) > 0) + { + newAngle = limits[1]; + } + + return newAngle; + } + + public static double limitEyeElevation(double elevation, ViewPropertyLimits viewLimits) + { + if (viewLimits == null) + { + String message = Logging.getMessage("nullValue.ViewLimitsIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + double newElevation = elevation; + double[] elevLimits = viewLimits.getEyeElevationLimits(); + + if (elevation < elevLimits[0]) + { + newElevation = elevLimits[0]; + } + else if (elevation > elevLimits[1]) + { + newElevation = elevLimits[1]; + } + return(newElevation); + } + + public static LatLon limitEyePositionLocation(Angle latitude, Angle longitude, ViewPropertyLimits viewLimits) + { + if (latitude == null || longitude == null) + { + String message = Logging.getMessage("nullValue.LatitudeOrLongitudeIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (viewLimits == null) + { + String message = Logging.getMessage("nullValue.ViewLimitsIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + Sector limits = viewLimits.getEyeLocationLimits(); + Angle newLatitude = latitude; + Angle newLongitude = longitude; + + if (latitude.compareTo(limits.minLatitude) < 0) + { + newLatitude = limits.minLatitude; + } + else if (latitude.compareTo(limits.maxLatitude) > 0) + { + newLatitude = limits.maxLatitude; + } + + if (longitude.compareTo(limits.minLongitude) < 0) + { + newLongitude = limits.minLongitude; + } + else if (longitude.compareTo(limits.maxLongitude) > 0) + { + newLongitude = limits.maxLongitude; + } + + return new LatLon(newLatitude, newLongitude); + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/view/OrbitViewCollisionSupport.java b/WorldWindAndroid/src/gov/nasa/worldwind/view/OrbitViewCollisionSupport.java new file mode 100644 index 0000000..8fb782f --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/view/OrbitViewCollisionSupport.java @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.view; + +import gov.nasa.worldwind.BasicView; +import gov.nasa.worldwind.avlist.AVKey; +import gov.nasa.worldwind.geom.*; +import gov.nasa.worldwind.globes.Globe; +import gov.nasa.worldwind.render.DrawContext; +import gov.nasa.worldwind.util.Logging; + +/** + * @author dcollins + * @version $Id$ + */ +public class OrbitViewCollisionSupport +{ + private double collisionThreshold; + private int numIterations; + + public OrbitViewCollisionSupport() + { + setNumIterations(1); + } + + public double getCollisionThreshold() + { + return this.collisionThreshold; + } + + public void setCollisionThreshold(double collisionThreshold) + { + if (collisionThreshold < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", collisionThreshold); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.collisionThreshold = collisionThreshold; + } + + public int getNumIterations() + { + return this.numIterations; + } + + public void setNumIterations(int numIterations) + { + if (numIterations < 1) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", numIterations); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + this.numIterations = numIterations; + } + + public boolean isColliding(BasicView orbitView, double nearDistance, DrawContext dc) + { + if (orbitView == null) + { + String message = Logging.getMessage("nullValue.BasicViewIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (nearDistance < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", nearDistance); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dc == null) + { + String message = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + Globe globe = dc.getGlobe(); + if (globe == null) + { + String message = Logging.getMessage("nullValue.DrawingContextGlobeIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + Matrix modelviewInv = getModelviewInverse(dc, + orbitView.getLookAtPosition(), orbitView.getHeading(), orbitView.getTilt(), orbitView.getRoll(), + orbitView.getRange()); + if (modelviewInv != null) + { + // BasicView is colliding when its eye point is below the collision threshold. + double heightAboveSurface = computeViewHeightAboveSurface(dc, modelviewInv, + orbitView.getFieldOfView(), orbitView.getViewport(), nearDistance); + return heightAboveSurface < this.collisionThreshold; + } + + return false; + } + + public Position computeCenterPositionToResolveCollision(BasicView orbitView, double nearDistance, + DrawContext dc) + { + if (orbitView == null) + { + String message = Logging.getMessage("nullValue.BasicViewIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (nearDistance < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", nearDistance); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dc == null) + { + String message = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + Globe globe = dc.getGlobe(); + if (globe == null) + { + String message = Logging.getMessage("nullValue.DrawingContextGlobeIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + Position newCenter = null; + + for (int i = 0; i < this.numIterations; i++) + { + Matrix modelviewInv = getModelviewInverse(dc, + newCenter != null ? newCenter : orbitView.getLookAtPosition(), + orbitView.getHeading(), orbitView.getTilt(), orbitView.getRoll(), orbitView.getRange()); + if (modelviewInv != null) + { + double heightAboveSurface = computeViewHeightAboveSurface(dc, modelviewInv, + orbitView.getFieldOfView(), orbitView.getViewport(), nearDistance); + double adjustedHeight = heightAboveSurface - this.collisionThreshold; + if (adjustedHeight < 0) + { + newCenter = new Position( + newCenter != null ? newCenter : orbitView.getLookAtPosition(), + (newCenter != null ? newCenter.elevation : orbitView.getLookAtPosition().elevation) + - adjustedHeight); + } + } + } + + return newCenter; + } + + public Angle computePitchToResolveCollision(BasicView orbitView, double nearDistance, DrawContext dc) + { + if (orbitView == null) + { + String message = Logging.getMessage("nullValue.BasicViewIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (nearDistance < 0) + { + String message = Logging.getMessage("generic.ArgumentOutOfRange", nearDistance); + Logging.error(message); + throw new IllegalArgumentException(message); + } + if (dc == null) + { + String message = Logging.getMessage("nullValue.DrawContextIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + Globe globe = dc.getGlobe(); + if (globe == null) + { + String message = Logging.getMessage("nullValue.DrawingContextGlobeIsNull"); + Logging.error(message); + throw new IllegalArgumentException(message); + } + + Angle newPitch = null; + + for (int i = 0; i < this.numIterations; i++) + { + Matrix modelviewInv = getModelviewInverse(dc, + orbitView.getLookAtPosition(), orbitView.getHeading(), + newPitch != null ? newPitch : orbitView.getTilt(), orbitView.getRoll(), + orbitView.getRange()); + if (modelviewInv != null) + { + double heightAboveSurface = computeViewHeightAboveSurface(dc, modelviewInv, + orbitView.getFieldOfView(), orbitView.getViewport(), nearDistance); + double adjustedHeight = heightAboveSurface - this.collisionThreshold; + if (adjustedHeight < 0) + { + Vec4 eyePoint = getEyePoint(modelviewInv); + Vec4 centerPoint = globe.computePointFromPosition(orbitView.getLookAtPosition()); + if (eyePoint != null && centerPoint != null) + { + Position eyePos = globe.computePositionFromPoint(eyePoint); + // Compute the eye point required to resolve the collision. + Vec4 newEyePoint = globe.computePointFromPosition(eyePos.getLatitude(), eyePos.getLongitude(), + eyePos.elevation - adjustedHeight); + // Compute the pitch that corresponds with the elevation of the eye point + // (but not necessarily the latitude and longitude). + Vec4 normalAtCenter = new Vec4(); + globe.computeSurfaceNormalAtPoint(centerPoint, normalAtCenter); + Vec4 newEye_sub_center = newEyePoint.subtract3(centerPoint).normalize3(); + double dot = normalAtCenter.dot3(newEye_sub_center); + if (dot >= -1 || dot <= 1) + { + double angle = Math.acos(dot); + newPitch = Angle.fromRadians(angle); + } + } + } + } + } + + return newPitch; + } + + public static double computeViewHeightAboveSurface(DrawContext dc, Matrix modelviewInv, + Angle fieldOfView, Rect viewport, double nearDistance) + { + double height = Double.POSITIVE_INFINITY; + if (dc != null && modelviewInv != null && fieldOfView != null && viewport != null && nearDistance >= 0) + { + Vec4 eyePoint = getEyePoint(modelviewInv); + if (eyePoint != null) + { + double eyeHeight = computePointHeightAboveSurface(dc, eyePoint); + if (eyeHeight < height) + height = eyeHeight; + } + + Vec4 nearPoint = getPointOnNearPlane(modelviewInv, fieldOfView, viewport, nearDistance); + if (nearPoint != null) + { + double nearHeight = computePointHeightAboveSurface(dc, nearPoint); + if (nearHeight < height) + height = nearHeight; + } + } + return height; + } + + public static double computePointHeightAboveSurface(DrawContext dc, Vec4 point) + { + if (dc != null && dc.getGlobe() != null && point != null) + { + final Globe globe = dc.getGlobe(); + Position position = globe.computePositionFromPoint(point); + // Look for the surface geometry point at 'position'. + Vec4 pointOnGlobe = dc.getVisibleTerrain().getSurfacePoint(position.latitude, position.longitude, 0); + if (pointOnGlobe != null) { + Vec4 eyeElevation = new Vec4(); + eyeElevation.subtract3AndSet(point, pointOnGlobe); + return eyeElevation.getLength3(); + } + // Fallback to using globe elevation values. + Position surfacePosition = new Position(position, + globe.getElevation(position.getLatitude(), position.getLongitude()) * dc.getVerticalExaggeration()); + return position.elevation - surfacePosition.elevation; + } + return Double.POSITIVE_INFINITY; + } + + public static Matrix getModelviewInverse(DrawContext dc, + Position centerPosition, Angle heading, Angle pitch, Angle roll, double zoom) + { + if (dc != null && centerPosition != null && heading != null && pitch != null) + { + Matrix modelview = Matrix.fromIdentity(); + modelview.setLookAt(dc.getVisibleTerrain(), centerPosition.latitude, centerPosition.longitude, centerPosition.elevation, AVKey.CLAMP_TO_GROUND, zoom, + heading, pitch, roll); + if (modelview != null) + return modelview.invert(); + } + + return null; + } + + public static Vec4 getEyePoint(Matrix modelviewInv) + { + return modelviewInv != null ? Vec4.UNIT_W.transformBy4(modelviewInv) : null; + } + + public static Vec4 getPointOnNearPlane(Matrix modelviewInv, Angle fieldOfView, Rect viewport, + double nearDistance) + { + if (modelviewInv != null && fieldOfView != null && viewport != null && nearDistance >= 0) + { + // If either either the viewport width or height is zero, then fall back to an aspect ratio of 1. + // Otherwise, compute the standard aspect ratio. + double aspect = (viewport.width <= 0 || viewport.height <= 0) ? + 1d : (viewport.height / viewport.width); + double nearClipHeight = 2 * aspect * nearDistance * fieldOfView.tanHalfAngle(); + // Computes the point on the bottom center of the near clip plane. + Vec4 nearClipVec = new Vec4(0, -nearClipHeight / 2.0, -nearDistance, 1); + return nearClipVec.transformBy4(modelviewInv); + } + + return null; + } +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/view/OrbitViewLimits.java b/WorldWindAndroid/src/gov/nasa/worldwind/view/OrbitViewLimits.java new file mode 100644 index 0000000..69b5dad --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/view/OrbitViewLimits.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.view; + +import gov.nasa.worldwind.geom.Sector; + +/** + * OrbitViewLimits defines a restriction on the standard viewing parameters of an OrbitView. + * + * @author dcollins + * @version $Id$ + */ +public interface OrbitViewLimits extends ViewPropertyLimits +{ + /** + * Returns the Sector which limits the OrbitView center latitude and longitude. + * + * @return Sector which limits the center latitude and longitude. + */ + Sector getCenterLocationLimits(); + + /** + * Sets the Sector which will limit the OrbitView center latitude and longitude. + * + * @param sector Sector which will limit the center latitude and longitude. + * @throws IllegalArgumentException if sector is null. + */ + void setCenterLocationLimits(Sector sector); + + /** + * Returns the minimum and maximum values for the OrbitView center elevation. + * + * @return minimum and maximum allowable values for center elevation. + */ + double[] getCenterElevationLimits(); + + /** + * Sets the minimum and maximum values which will limit the OrbitView center elevation. + * + * @param minValue the minimum allowable value for center elevation. + * @param maxValue the maximum allowable value for center elevation. + */ + void setCenterElevationLimits(double minValue, double maxValue); + + + + /** + * Returns the minimum and maximum values for the OrbitView zoom property. + * + * @return minimum and maximum allowable values for zoom. + */ + double[] getZoomLimits(); + + /** + * Sets the minimum and maximum values which will limit the OrbitView zoom property. + * + * @param minValue the mimimum allowable value for zoom. + * @param maxValue the maximum allowable value for zoom. + */ + void setZoomLimits(double minValue, double maxValue); + +} diff --git a/WorldWindAndroid/src/gov/nasa/worldwind/view/ViewPropertyLimits.java b/WorldWindAndroid/src/gov/nasa/worldwind/view/ViewPropertyLimits.java new file mode 100644 index 0000000..867d088 --- /dev/null +++ b/WorldWindAndroid/src/gov/nasa/worldwind/view/ViewPropertyLimits.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2014 United States Government as represented by the Administrator of the + * National Aeronautics and Space Administration. + * All Rights Reserved. + */ +package gov.nasa.worldwind.view; + +import gov.nasa.worldwind.geom.Angle; +import gov.nasa.worldwind.geom.Sector; + +/** + * @author jym + * @version $Id$ + */ +public interface ViewPropertyLimits +{ + /** + * Sets the Sector which will limit the View eye position latitude and longitude. + * + * @param sector Sector which will limit the eye position latitude and longitude. + * @throws IllegalArgumentException if sector is null. + */ + void setEyeLocationLimits(Sector sector); + /** + * Returns the Sector which limits the View eye position latitude and longitude. + * + * @return Sector which limits the eye position latitude and longitude. + */ + Sector getEyeLocationLimits(); + + /** + * Returns the minimum and maximum values for the View elevation. + * + * @return minimum and maximum allowable values for the elevation. + */ + double[] getEyeElevationLimits(); + + /** + * Sets the minimum and maximum values for the View elevation. + * + * @param minValue The minimum elevation. + * @param maxValue The maximum elevation. + */ + void setEyeElevationLimits(double minValue, double maxValue); + + /** + * Returns the minimum and maximum Angles for the OrbitView heading property. + * + * @return minimum and maximum allowable Angles for heading. + */ + Angle[] getHeadingLimits(); + + /** + * Sets the minimum and maximum Angles which will limit the OrbitView heading property. + * + * @param minAngle the minimum allowable angle for heading. + * @param maxAngle the maximum allowable angle for heading. + * @throws IllegalArgumentException if either minAngle or maxAngle is null. + */ + void setHeadingLimits(Angle minAngle, Angle maxAngle); + + /** + * Returns the minimum and maximum Angles for the OrbitView pitch property. + * + * @return minimum and maximum allowable Angles for pitch. + */ + Angle[] getPitchLimits(); + + /** + * Sets the minimum and maximum Angles which will limit the OrbitView pitch property. + * + * @param minAngle the minimum allowable angle for pitch. + * @param maxAngle the maximum allowable angle for pitch. + * @throws IllegalArgumentException if either minAngle or maxAngle is null. + */ + void setPitchLimits(Angle minAngle, Angle maxAngle); + + /** + * Returns the minimum and maximum Angles for the OrbitView roll property. + * + * @return minimum and maximum allowable Angles for roll. + */ + Angle[] getRollLimits(); + + /** + * Sets the minimum and maximum Angles which will limit the OrbitView roll property. + * + * @param minAngle the minimum allowable angle for roll. + * @param maxAngle the maximum allowable angle for roll. + * + * @throws IllegalArgumentException if either minAngle or maxAngle is null. + */ + void setRollLimits(Angle minAngle, Angle maxAngle); + + +} diff --git a/WorldWindAndroid/src/images/earth-map-512x256.pkm b/WorldWindAndroid/src/images/earth-map-512x256.pkm new file mode 100644 index 0000000..40d1019 Binary files /dev/null and b/WorldWindAndroid/src/images/earth-map-512x256.pkm differ diff --git a/WorldWindAndroid/src/images/earth-map-512x256_alpha.pkm b/WorldWindAndroid/src/images/earth-map-512x256_alpha.pkm new file mode 100644 index 0000000..5d851e3 Binary files /dev/null and b/WorldWindAndroid/src/images/earth-map-512x256_alpha.pkm differ diff --git a/WorldWindAndroid/src/images/notched-compass_mip_0.pkm b/WorldWindAndroid/src/images/notched-compass_mip_0.pkm new file mode 100644 index 0000000..a3fc674 Binary files /dev/null and b/WorldWindAndroid/src/images/notched-compass_mip_0.pkm differ diff --git a/WorldWindAndroid/src/images/notched-compass_mip_0_alpha.pkm b/WorldWindAndroid/src/images/notched-compass_mip_0_alpha.pkm new file mode 100644 index 0000000..0a9413e Binary files /dev/null and b/WorldWindAndroid/src/images/notched-compass_mip_0_alpha.pkm differ diff --git a/WorldWindAndroid/src/images/notched-compass_mip_1.pkm b/WorldWindAndroid/src/images/notched-compass_mip_1.pkm new file mode 100644 index 0000000..feacd7b Binary files /dev/null and b/WorldWindAndroid/src/images/notched-compass_mip_1.pkm differ diff --git a/WorldWindAndroid/src/images/notched-compass_mip_1_alpha.pkm b/WorldWindAndroid/src/images/notched-compass_mip_1_alpha.pkm new file mode 100644 index 0000000..39db176 Binary files /dev/null and b/WorldWindAndroid/src/images/notched-compass_mip_1_alpha.pkm differ diff --git a/WorldWindAndroid/src/images/notched-compass_mip_2.pkm b/WorldWindAndroid/src/images/notched-compass_mip_2.pkm new file mode 100644 index 0000000..763a078 Binary files /dev/null and b/WorldWindAndroid/src/images/notched-compass_mip_2.pkm differ diff --git a/WorldWindAndroid/src/images/notched-compass_mip_2_alpha.pkm b/WorldWindAndroid/src/images/notched-compass_mip_2_alpha.pkm new file mode 100644 index 0000000..198947b Binary files /dev/null and b/WorldWindAndroid/src/images/notched-compass_mip_2_alpha.pkm differ diff --git a/WorldWindAndroid/src/images/notched-compass_mip_3.pkm b/WorldWindAndroid/src/images/notched-compass_mip_3.pkm new file mode 100644 index 0000000..a57415a Binary files /dev/null and b/WorldWindAndroid/src/images/notched-compass_mip_3.pkm differ diff --git a/WorldWindAndroid/src/images/notched-compass_mip_3_alpha.pkm b/WorldWindAndroid/src/images/notched-compass_mip_3_alpha.pkm new file mode 100644 index 0000000..84e1d60 Binary files /dev/null and b/WorldWindAndroid/src/images/notched-compass_mip_3_alpha.pkm differ diff --git a/WorldWindAndroid/src/images/notched-compass_mip_4.pkm b/WorldWindAndroid/src/images/notched-compass_mip_4.pkm new file mode 100644 index 0000000..1f0cad5 Binary files /dev/null and b/WorldWindAndroid/src/images/notched-compass_mip_4.pkm differ diff --git a/WorldWindAndroid/src/images/notched-compass_mip_4_alpha.pkm b/WorldWindAndroid/src/images/notched-compass_mip_4_alpha.pkm new file mode 100644 index 0000000..1159f2e Binary files /dev/null and b/WorldWindAndroid/src/images/notched-compass_mip_4_alpha.pkm differ diff --git a/WorldWindAndroid/src/images/notched-compass_mip_5.pkm b/WorldWindAndroid/src/images/notched-compass_mip_5.pkm new file mode 100644 index 0000000..e6e38ac Binary files /dev/null and b/WorldWindAndroid/src/images/notched-compass_mip_5.pkm differ diff --git a/WorldWindAndroid/src/images/notched-compass_mip_5_alpha.pkm b/WorldWindAndroid/src/images/notched-compass_mip_5_alpha.pkm new file mode 100644 index 0000000..d94c302 Binary files /dev/null and b/WorldWindAndroid/src/images/notched-compass_mip_5_alpha.pkm differ diff --git a/WorldWindAndroid/src/images/notched-compass_mip_6.pkm b/WorldWindAndroid/src/images/notched-compass_mip_6.pkm new file mode 100644 index 0000000..9953c1a Binary files /dev/null and b/WorldWindAndroid/src/images/notched-compass_mip_6.pkm differ diff --git a/WorldWindAndroid/src/images/notched-compass_mip_6_alpha.pkm b/WorldWindAndroid/src/images/notched-compass_mip_6_alpha.pkm new file mode 100644 index 0000000..8b1b341 Binary files /dev/null and b/WorldWindAndroid/src/images/notched-compass_mip_6_alpha.pkm differ diff --git a/WorldWindAndroid/src/images/notched-compass_mip_7.pkm b/WorldWindAndroid/src/images/notched-compass_mip_7.pkm new file mode 100644 index 0000000..af1db09 Binary files /dev/null and b/WorldWindAndroid/src/images/notched-compass_mip_7.pkm differ diff --git a/WorldWindAndroid/src/images/notched-compass_mip_7_alpha.pkm b/WorldWindAndroid/src/images/notched-compass_mip_7_alpha.pkm new file mode 100644 index 0000000..9145722 Binary files /dev/null and b/WorldWindAndroid/src/images/notched-compass_mip_7_alpha.pkm differ diff --git a/WorldWindAndroid/src/images/notched-compass_mip_8.pkm b/WorldWindAndroid/src/images/notched-compass_mip_8.pkm new file mode 100644 index 0000000..a336d9d Binary files /dev/null and b/WorldWindAndroid/src/images/notched-compass_mip_8.pkm differ diff --git a/WorldWindAndroid/src/images/notched-compass_mip_8_alpha.pkm b/WorldWindAndroid/src/images/notched-compass_mip_8_alpha.pkm new file mode 100644 index 0000000..0376144 Binary files /dev/null and b/WorldWindAndroid/src/images/notched-compass_mip_8_alpha.pkm differ diff --git a/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024.png b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024.png new file mode 100644 index 0000000..d28f3c5 Binary files /dev/null and b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024.png differ diff --git a/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_0.pkm b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_0.pkm new file mode 100644 index 0000000..e47c778 Binary files /dev/null and b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_0.pkm differ diff --git a/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_1.pkm b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_1.pkm new file mode 100644 index 0000000..304c82e Binary files /dev/null and b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_1.pkm differ diff --git a/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_10.pkm b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_10.pkm new file mode 100644 index 0000000..c8129b5 Binary files /dev/null and b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_10.pkm differ diff --git a/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_11.pkm b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_11.pkm new file mode 100644 index 0000000..ae11618 Binary files /dev/null and b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_11.pkm differ diff --git a/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_2.pkm b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_2.pkm new file mode 100644 index 0000000..15ab95c Binary files /dev/null and b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_2.pkm differ diff --git a/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_3.pkm b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_3.pkm new file mode 100644 index 0000000..32f7aa8 Binary files /dev/null and b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_3.pkm differ diff --git a/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_4.pkm b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_4.pkm new file mode 100644 index 0000000..f1f718c Binary files /dev/null and b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_4.pkm differ diff --git a/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_5.pkm b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_5.pkm new file mode 100644 index 0000000..ea1f226 Binary files /dev/null and b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_5.pkm differ diff --git a/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_6.pkm b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_6.pkm new file mode 100644 index 0000000..34a8275 Binary files /dev/null and b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_6.pkm differ diff --git a/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_7.pkm b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_7.pkm new file mode 100644 index 0000000..bbbb7d8 Binary files /dev/null and b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_7.pkm differ diff --git a/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_8.pkm b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_8.pkm new file mode 100644 index 0000000..f16611d Binary files /dev/null and b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_8.pkm differ diff --git a/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_9.pkm b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_9.pkm new file mode 100644 index 0000000..0e32cee Binary files /dev/null and b/WorldWindAndroid/src/images/world.topo.bathy.200405.3x2048x1024_mip_9.pkm differ diff --git a/WorldWindAndroid/src/shaders/CompassLayerTexture.frag b/WorldWindAndroid/src/shaders/CompassLayerTexture.frag deleted file mode 100644 index fcf9ca5..0000000 --- a/WorldWindAndroid/src/shaders/CompassLayerTexture.frag +++ /dev/null @@ -1,13 +0,0 @@ -precision mediump float; - -varying vec2 vTextureCoord; - -uniform sampler2D sTexture; - -/* - * OpenGL ES fragment shader entry point. Called for each fragment rasterized when this shader's program is bound. - */ -void main() -{ - gl_FragColor = texture2D(sTexture, vTextureCoord); -} diff --git a/WorldWindAndroid/src/shaders/ScalebarLayerColor.frag b/WorldWindAndroid/src/shaders/ScalebarLayerColor.frag deleted file mode 100644 index b90c8c0..0000000 --- a/WorldWindAndroid/src/shaders/ScalebarLayerColor.frag +++ /dev/null @@ -1,16 +0,0 @@ -precision mediump float; - -/* - * Input varying vector from TiledTessellatorPick.vert defining the color for each primitive (triangle). This is - * specified for each vertex and is interpolated for each rasterized fragment of each primitive. - */ -varying vec4 primColor; - -/* - * OpenGL ES fragment shader entry point. Called for each fragment rasterized when this shader's program is bound. - */ -void main() -{ - /* Assign the fragment color to the varying vertex color. */ - gl_FragColor = primColor; -} diff --git a/WorldWindAndroid/src/shaders/ScalebarLayerColor.vert b/WorldWindAndroid/src/shaders/ScalebarLayerColor.vert deleted file mode 100644 index b6c3ecc..0000000 --- a/WorldWindAndroid/src/shaders/ScalebarLayerColor.vert +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Input vertex attribute defining the surface vertex point in model coordinates. This attribute is specified in - * TODO. - */ -attribute vec4 vertexPoint; -/* - * Input vertex attribute defining the unique RGB color of each primitive. This attribute is specified in TODO. Although - * this attribute can vary per-vertex, it is assumed that either (a) this is a constant value for the entire primitive, - * or (b) the same color is assigned to each triangle vertex. - */ -uniform vec4 uColor; -/* - * Input uniform matrix defining the current modelview-projection transform matrix. Maps model coordinates to eye - * coordinates. - */ -uniform mat4 mvpMatrix; - -/* - * Output variable vector to TiledTessellatorPick.frag defining the color for each primitive (triangle). This is - * specified for each vertex and is interpolated for each rasterized fragment of each primitive. - */ -varying vec4 primColor; - -/* - * OpenGL ES vertex shader entry point. Called for each vertex processed when this shader's program is bound. - */ -void main() -{ - /* Transform the surface vertex point from model coordinates to eye coordinates. */ - gl_Position = mvpMatrix * vertexPoint; - - /* Assign the varying fragment color to the current vertex's color. */ - primColor = uColor; -} diff --git a/WorldWindAndroid/src/shaders/SkyGradientLayer.frag b/WorldWindAndroid/src/shaders/SkyGradientLayer.frag deleted file mode 100644 index b90c8c0..0000000 --- a/WorldWindAndroid/src/shaders/SkyGradientLayer.frag +++ /dev/null @@ -1,16 +0,0 @@ -precision mediump float; - -/* - * Input varying vector from TiledTessellatorPick.vert defining the color for each primitive (triangle). This is - * specified for each vertex and is interpolated for each rasterized fragment of each primitive. - */ -varying vec4 primColor; - -/* - * OpenGL ES fragment shader entry point. Called for each fragment rasterized when this shader's program is bound. - */ -void main() -{ - /* Assign the fragment color to the varying vertex color. */ - gl_FragColor = primColor; -} diff --git a/WorldWindAndroid/src/shaders/SkyGradientLayer.vert b/WorldWindAndroid/src/shaders/SkyGradientLayer.vert deleted file mode 100644 index 8961ddc..0000000 --- a/WorldWindAndroid/src/shaders/SkyGradientLayer.vert +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Input vertex attribute defining the surface vertex point in model coordinates. This attribute is specified in - * TODO. - */ -attribute vec4 vertexPoint; -/* - * Input vertex attribute defining the unique RGB color of each primitive. This attribute is specified in TODO. Although - * this attribute can vary per-vertex, it is assumed that either (a) this is a constant value for the entire primitive, - * or (b) the same color is assigned to each triangle vertex. - */ -attribute vec4 vertexColor; -/* - * Input uniform matrix defining the current modelview-projection transform matrix. Maps model coordinates to eye - * coordinates. - */ -uniform mat4 mvpMatrix; - -/* - * Output variable vector to TiledTessellatorPick.frag defining the color for each primitive (triangle). This is - * specified for each vertex and is interpolated for each rasterized fragment of each primitive. - */ -varying vec4 primColor; - -/* - * OpenGL ES vertex shader entry point. Called for each vertex processed when this shader's program is bound. - */ -void main() -{ - /* Transform the surface vertex point from model coordinates to eye coordinates. */ - gl_Position = mvpMatrix * vertexPoint; - - /* Assign the varying fragment color to the current vertex's color. */ - primColor = vertexColor; -} diff --git a/WorldWindAndroid/src/shaders/TextRenderer.vert b/WorldWindAndroid/src/shaders/TextRenderer.vert deleted file mode 100644 index de84d43..0000000 --- a/WorldWindAndroid/src/shaders/TextRenderer.vert +++ /dev/null @@ -1,25 +0,0 @@ -uniform vec4 uTextureColor; -/* - * Input vertex attribute defining the surface vertex point in model coordinates. This attribute is specified in - * TODO. - */ -attribute vec4 vertexPoint; -attribute vec2 aTextureCoord; -/* - * Input uniform matrix defining the current modelview-projection transform matrix. Maps model coordinates to eye - * coordinates. - */ -uniform mat4 mvpMatrix; -varying vec2 vTextureCoord; -varying vec4 vTextureColor; - -/* - * OpenGL ES vertex shader entry point. Called for each vertex processed when this shader's program is bound. - */ -void main() -{ - /* Transform the surface vertex point from model coordinates to eye coordinates. */ - gl_Position = mvpMatrix * vertexPoint; - vTextureCoord = aTextureCoord; - vTextureColor = uTextureColor; -} diff --git a/WorldWindAndroid/src/shaders/TiledTessellatorPick.frag b/WorldWindAndroid/src/shaders/TiledTessellatorPick.frag deleted file mode 100644 index 1416096..0000000 --- a/WorldWindAndroid/src/shaders/TiledTessellatorPick.frag +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2012 United States Government as represented by the Administrator of the - * National Aeronautics and Space Administration. - * All Rights Reserved. - */ - -/* - * OpenGL ES Shading Language v1.00 fragment shader for TiledTessellator picking. Displays either a constant unique RGB - * color or a unique RGB color assigned to each primitive (triangle). - * - * version $Id: TiledTessellatorPick.frag 733 2012-09-02 17:15:09Z dcollins $ - */ - -precision mediump float; - -/* - * Input varying vector from TiledTessellatorPick.vert defining the color for each primitive (triangle). This is - * specified for each vertex and is interpolated for each rasterized fragment of each primitive. - */ -varying vec4 primColor; - -/* - * OpenGL ES fragment shader entry point. Called for each fragment rasterized when this shader's program is bound. - */ -void main() -{ - /* Assign the fragment color to the varying vertex color. */ - gl_FragColor = primColor; -} diff --git a/WorldWindAndroid/src/shaders/WorldMapLayerColor.vert b/WorldWindAndroid/src/shaders/WorldMapLayerColor.vert deleted file mode 100644 index b6c3ecc..0000000 --- a/WorldWindAndroid/src/shaders/WorldMapLayerColor.vert +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Input vertex attribute defining the surface vertex point in model coordinates. This attribute is specified in - * TODO. - */ -attribute vec4 vertexPoint; -/* - * Input vertex attribute defining the unique RGB color of each primitive. This attribute is specified in TODO. Although - * this attribute can vary per-vertex, it is assumed that either (a) this is a constant value for the entire primitive, - * or (b) the same color is assigned to each triangle vertex. - */ -uniform vec4 uColor; -/* - * Input uniform matrix defining the current modelview-projection transform matrix. Maps model coordinates to eye - * coordinates. - */ -uniform mat4 mvpMatrix; - -/* - * Output variable vector to TiledTessellatorPick.frag defining the color for each primitive (triangle). This is - * specified for each vertex and is interpolated for each rasterized fragment of each primitive. - */ -varying vec4 primColor; - -/* - * OpenGL ES vertex shader entry point. Called for each vertex processed when this shader's program is bound. - */ -void main() -{ - /* Transform the surface vertex point from model coordinates to eye coordinates. */ - gl_Position = mvpMatrix * vertexPoint; - - /* Assign the varying fragment color to the current vertex's color. */ - primColor = uColor; -} diff --git a/WorldWindowApplicationSample/AndroidManifest.xml b/WorldWindowApplicationSample/AndroidManifest.xml index b94c140..8a4421d 100644 --- a/WorldWindowApplicationSample/AndroidManifest.xml +++ b/WorldWindowApplicationSample/AndroidManifest.xml @@ -5,21 +5,15 @@ All Rights Reserved. $Id: AndroidManifest.xml 59 2011-09-20 18:41:08Z dcollins $ ---> - +--> - + - + - - - + + + - - + + + + + + + - + - - + + diff --git a/WorldWindowApplicationSample/libs/android-support-v4.jar b/WorldWindowApplicationSample/libs/android-support-v4.jar deleted file mode 100644 index 428bdbc..0000000 Binary files a/WorldWindowApplicationSample/libs/android-support-v4.jar and /dev/null differ diff --git a/WorldWindowApplicationSample/libs/crouton-1.7.jar b/WorldWindowApplicationSample/libs/crouton-1.7.jar deleted file mode 100644 index 5299a3a..0000000 Binary files a/WorldWindowApplicationSample/libs/crouton-1.7.jar and /dev/null differ diff --git a/WorldWindowApplicationSample/libs/okhttp_10-05.jar b/WorldWindowApplicationSample/libs/okhttp_10-05.jar deleted file mode 100644 index ba080ec..0000000 Binary files a/WorldWindowApplicationSample/libs/okhttp_10-05.jar and /dev/null differ diff --git a/WorldWindowApplicationSample/pom.xml b/WorldWindowApplicationSample/pom.xml new file mode 100644 index 0000000..33f3020 --- /dev/null +++ b/WorldWindowApplicationSample/pom.xml @@ -0,0 +1,82 @@ + + + + + + 4.0.0 + + + com.github.trilogisit.worldwindandroid + parent + 1.0.0 + + + sample + apk + + WorldWindAndroid Sample + + + https://github.com/TrilogisIT/WorldWindAndroid.git + scm:git:git://github.com/TrilogisIT/WorldWindAndroid.git + scm:git:git://github.com/TrilogisIT/WorldWindAndroid.git + + + + https://www.github.com/TrilogisIT/WorldWindAndroid/issues + GitHub Issues + + + + + android + android + 4.4.2_r3 + provided + + + commons-logging + commons-logging + + + + + android.support + compatibility-v4 + 19.0.1 + + + com.github.trilogisit.worldwindandroid + library + ${project.version} + aar + + + com.github.masdennis.rajawali + rajawali + 1.0.0 + aar + + + + + + + com.jayway.maven.plugins.android.generation2 + android-maven-plugin + + + + + + + third.party.closed.source.repo + file://${basedir}/../maven_repo_3rd_party + + + diff --git a/WorldWindowApplicationSample/res/drawable-hdpi/drawer_shadow.9.png b/WorldWindowApplicationSample/res/drawable-hdpi/drawer_shadow.9.png new file mode 100644 index 0000000..236bff5 Binary files /dev/null and b/WorldWindowApplicationSample/res/drawable-hdpi/drawer_shadow.9.png differ diff --git a/WorldWindowApplicationSample/res/drawable-hdpi/ic_action_new.png b/WorldWindowApplicationSample/res/drawable-hdpi/ic_action_new.png new file mode 100644 index 0000000..3a525cd Binary files /dev/null and b/WorldWindowApplicationSample/res/drawable-hdpi/ic_action_new.png differ diff --git a/WorldWindowApplicationSample/res/drawable-hdpi/ic_action_settings.png b/WorldWindowApplicationSample/res/drawable-hdpi/ic_action_settings.png new file mode 100644 index 0000000..0eb78f7 Binary files /dev/null and b/WorldWindowApplicationSample/res/drawable-hdpi/ic_action_settings.png differ diff --git a/WorldWindowApplicationSample/res/drawable-hdpi/ic_drawer.png b/WorldWindowApplicationSample/res/drawable-hdpi/ic_drawer.png new file mode 100644 index 0000000..c59f601 Binary files /dev/null and b/WorldWindowApplicationSample/res/drawable-hdpi/ic_drawer.png differ diff --git a/WorldWindowApplicationSample/res/drawable-mdpi/drawer_shadow.9.png b/WorldWindowApplicationSample/res/drawable-mdpi/drawer_shadow.9.png new file mode 100644 index 0000000..ffe3a28 Binary files /dev/null and b/WorldWindowApplicationSample/res/drawable-mdpi/drawer_shadow.9.png differ diff --git a/WorldWindowApplicationSample/res/drawable-mdpi/ic_action_new.png b/WorldWindowApplicationSample/res/drawable-mdpi/ic_action_new.png new file mode 100644 index 0000000..da506ca Binary files /dev/null and b/WorldWindowApplicationSample/res/drawable-mdpi/ic_action_new.png differ diff --git a/WorldWindowApplicationSample/res/drawable-mdpi/ic_action_settings.png b/WorldWindowApplicationSample/res/drawable-mdpi/ic_action_settings.png new file mode 100644 index 0000000..c290e59 Binary files /dev/null and b/WorldWindowApplicationSample/res/drawable-mdpi/ic_action_settings.png differ diff --git a/WorldWindowApplicationSample/res/drawable-mdpi/ic_drawer.png b/WorldWindowApplicationSample/res/drawable-mdpi/ic_drawer.png new file mode 100644 index 0000000..1ed2c56 Binary files /dev/null and b/WorldWindowApplicationSample/res/drawable-mdpi/ic_drawer.png differ diff --git a/WorldWindowApplicationSample/res/drawable-xhdpi/drawer_shadow.9.png b/WorldWindowApplicationSample/res/drawable-xhdpi/drawer_shadow.9.png new file mode 100644 index 0000000..fabe9d9 Binary files /dev/null and b/WorldWindowApplicationSample/res/drawable-xhdpi/drawer_shadow.9.png differ diff --git a/WorldWindowApplicationSample/res/drawable-xhdpi/ic_action_new.png b/WorldWindowApplicationSample/res/drawable-xhdpi/ic_action_new.png new file mode 100644 index 0000000..6038bee Binary files /dev/null and b/WorldWindowApplicationSample/res/drawable-xhdpi/ic_action_new.png differ diff --git a/WorldWindowApplicationSample/res/drawable-xhdpi/ic_action_settings.png b/WorldWindowApplicationSample/res/drawable-xhdpi/ic_action_settings.png new file mode 100644 index 0000000..999d0f0 Binary files /dev/null and b/WorldWindowApplicationSample/res/drawable-xhdpi/ic_action_settings.png differ diff --git a/WorldWindowApplicationSample/res/drawable-xhdpi/ic_drawer.png b/WorldWindowApplicationSample/res/drawable-xhdpi/ic_drawer.png new file mode 100644 index 0000000..a5fa74d Binary files /dev/null and b/WorldWindowApplicationSample/res/drawable-xhdpi/ic_drawer.png differ diff --git a/WorldWindowApplicationSample/res/drawable-xxhdpi/drawer_shadow.9.png b/WorldWindowApplicationSample/res/drawable-xxhdpi/drawer_shadow.9.png new file mode 100644 index 0000000..b91e9d7 Binary files /dev/null and b/WorldWindowApplicationSample/res/drawable-xxhdpi/drawer_shadow.9.png differ diff --git a/WorldWindowApplicationSample/res/drawable-xxhdpi/ic_action_new.png b/WorldWindowApplicationSample/res/drawable-xxhdpi/ic_action_new.png new file mode 100644 index 0000000..c0cd3d9 Binary files /dev/null and b/WorldWindowApplicationSample/res/drawable-xxhdpi/ic_action_new.png differ diff --git a/WorldWindowApplicationSample/res/drawable-xxhdpi/ic_action_settings.png b/WorldWindowApplicationSample/res/drawable-xxhdpi/ic_action_settings.png new file mode 100644 index 0000000..530227e Binary files /dev/null and b/WorldWindowApplicationSample/res/drawable-xxhdpi/ic_action_settings.png differ diff --git a/WorldWindowApplicationSample/res/drawable-xxhdpi/ic_drawer.png b/WorldWindowApplicationSample/res/drawable-xxhdpi/ic_drawer.png new file mode 100644 index 0000000..9c4685d Binary files /dev/null and b/WorldWindowApplicationSample/res/drawable-xxhdpi/ic_drawer.png differ diff --git a/WorldWindowApplicationSample/res/layout/main.xml b/WorldWindowApplicationSample/res/layout/main.xml index 2b07347..bede6bc 100644 --- a/WorldWindowApplicationSample/res/layout/main.xml +++ b/WorldWindowApplicationSample/res/layout/main.xml @@ -1,70 +1,52 @@ - + - - - - + android:layout_height="match_parent"> - - - - - + android:layout_height="24dp" + android:layout_alignParentBottom="true" + android:background="@android:color/background_light"/> - - - + + + + + + + - + - + - - - \ No newline at end of file + \ No newline at end of file diff --git a/WorldWindowApplicationSample/res/layout/toc_view_dialog.xml b/WorldWindowApplicationSample/res/layout/toc_view_dialog.xml index 5558ce8..8639706 100644 --- a/WorldWindowApplicationSample/res/layout/toc_view_dialog.xml +++ b/WorldWindowApplicationSample/res/layout/toc_view_dialog.xml @@ -1,15 +1,15 @@ - + android:layout_height="match_parent" + android:orientation="vertical"> - \ No newline at end of file + + + + +

    Parameter + * NameValue
    GL_TEXTURE_MIN_FILTERGL_LINEAR_MIPMAP_LINEAR + * if useLinearFilter and useMipmaps are both true, GL_LINEAR if + * useLinearFilter is true and useMipmaps is false, and GL_NEAREST if + * useLinearFilter is false.
    GL_TEXTURE_MAG_FILTERGL_LINEAR + * if useLinearFilter is true, GL_NEAREST if useLinearFilter is + * false.
    GL_TEXTURE_WRAP_SGL_CLAMP_TO_EDGE
    GL_TEXTURE_WRAP_TGL_CLAMP_TO_EDGE