From cffa78633723a514b16475559009eaf71889762a Mon Sep 17 00:00:00 2001 From: tengge <930372551@qq.com> Date: Thu, 13 Aug 2020 19:34:54 +0800 Subject: [PATCH] readd WebWorldWind --- package-lock.json | 240 + package.json | 4 +- .../WebWorldWind}/images/powered-by-bing.png | Bin .../WebWorldWind}/images/stars.json | 0 .../WebWorldWind}/images/sunTexture.png | Bin web/test/WebWorldWind/index.html | 37 + web/test/WebWorldWind/rollup.config.js | 49 + .../src/BasicWorldWindowController.js | 408 ++ web/test/WebWorldWind/src/README.md | 7 + web/test/WebWorldWind/src/WorldWind.js | 517 ++ web/test/WebWorldWind/src/WorldWindow.js | 1521 +++++ .../WebWorldWind/src/WorldWindowController.js | 120 + .../src/cache/GpuResourceCache.js | 272 + .../WebWorldWind/src/cache/MemoryCache.js | 347 ++ .../src/cache/MemoryCacheListener.js | 56 + .../WebWorldWind/src/error/AbstractError.js | 53 + .../WebWorldWind/src/error/ArgumentError.js | 46 + .../src/error/NotYetImplementedError.js | 46 + .../src/error/UnsupportedOperationError.js | 48 + web/test/WebWorldWind/src/geom/Angle.js | 216 + web/test/WebWorldWind/src/geom/BoundingBox.js | 579 ++ web/test/WebWorldWind/src/geom/Frustum.js | 315 + web/test/WebWorldWind/src/geom/Line.js | 140 + web/test/WebWorldWind/src/geom/Location.js | 961 +++ web/test/WebWorldWind/src/geom/Matrix.js | 1908 ++++++ web/test/WebWorldWind/src/geom/Matrix3.js | 228 + web/test/WebWorldWind/src/geom/Plane.js | 287 + web/test/WebWorldWind/src/geom/Position.js | 207 + web/test/WebWorldWind/src/geom/Rectangle.js | 161 + web/test/WebWorldWind/src/geom/Sector.js | 536 ++ web/test/WebWorldWind/src/geom/Vec2.js | 321 + web/test/WebWorldWind/src/geom/Vec3.js | 538 ++ .../src/gesture/ClickRecognizer.js | 162 + .../src/gesture/DragRecognizer.js | 108 + .../src/gesture/GestureRecognizer.js | 793 +++ .../WebWorldWind/src/gesture/PanRecognizer.js | 132 + .../src/gesture/PinchRecognizer.js | 169 + .../src/gesture/RotationRecognizer.js | 172 + .../WebWorldWind/src/gesture/TapRecognizer.js | 182 + .../src/gesture/TiltRecognizer.js | 120 + web/test/WebWorldWind/src/gesture/Touch.js | 112 + .../src/globe/ArcgisElevationCoverage.js | 115 + .../src/globe/ArcgisElevationWorker.js | 2219 +++++++ .../src/globe/AsterV2ElevationCoverage.js | 47 + .../src/globe/EarthElevationModel.js | 44 + .../src/globe/ElevationCoverage.js | 168 + .../WebWorldWind/src/globe/ElevationImage.js | 354 ++ .../WebWorldWind/src/globe/ElevationModel.js | 417 ++ .../src/globe/GebcoElevationCoverage.js | 47 + web/test/WebWorldWind/src/globe/Globe.js | 651 ++ web/test/WebWorldWind/src/globe/Terrain.js | 226 + .../WebWorldWind/src/globe/TerrainTile.js | 230 + .../WebWorldWind/src/globe/TerrainTileList.js | 81 + .../WebWorldWind/src/globe/Tessellator.js | 1528 +++++ .../src/globe/TiledElevationCoverage.js | 635 ++ .../src/globe/UsgsNedElevationCoverage.js | 49 + .../src/globe/UsgsNedHiElevationCoverage.js | 49 + .../WebWorldWind/src/layer/AtmosphereLayer.js | 313 + web/test/WebWorldWind/src/layer/Layer.js | 158 + .../src/layer/MercatorTiledImageLayer.js | 67 + .../WebWorldWind/src/layer/StarFieldLayer.js | 428 ++ .../WebWorldWind/src/layer/TiledImageLayer.js | 542 ++ web/test/WebWorldWind/src/layer/XYZLayer.js | 30 + .../src/navigate/LookAtNavigator.js | 54 + .../WebWorldWind/src/navigate/Navigator.js | 53 + .../WebWorldWind/src/pick/PickedObject.js | 78 + .../WebWorldWind/src/pick/PickedObjectList.js | 114 + .../src/projections/GeographicProjection.js | 252 + .../src/projections/ProjectionWgs84.js | 311 + .../WebWorldWind/src/render/DrawContext.js | 1567 +++++ .../src/render/FramebufferTexture.js | 147 + .../src/render/FramebufferTile.js | 128 + .../src/render/FramebufferTileController.js | 251 + web/test/WebWorldWind/src/render/ImageTile.js | 137 + .../src/render/OrderedRenderable.js | 75 + .../WebWorldWind/src/render/Renderable.js | 76 + .../src/render/ScreenCreditController.js | 128 + .../src/render/SurfaceRenderable.js | 61 + .../WebWorldWind/src/render/SurfaceTile.js | 72 + .../src/render/SurfaceTileRenderer.js | 189 + .../WebWorldWind/src/render/TextRenderer.js | 307 + web/test/WebWorldWind/src/render/Texture.js | 187 + .../WebWorldWind/src/render/TextureTile.js | 84 + .../src/shaders/AtmosphereProgram.js | 320 + .../WebWorldWind/src/shaders/BasicProgram.js | 127 + .../src/shaders/BasicTextureProgram.js | 256 + .../WebWorldWind/src/shaders/GpuProgram.js | 281 + .../WebWorldWind/src/shaders/GpuShader.js | 99 + .../WebWorldWind/src/shaders/GroundProgram.js | 61 + .../WebWorldWind/src/shaders/SkyProgram.js | 62 + .../src/shaders/StarFieldProgram.js | 172 + .../src/shaders/SurfaceTileRendererProgram.js | 212 + .../src/shaders/glsl/basic_fragment.glsl | 23 + .../shaders/glsl/basic_texture_fragment.glsl | 47 + .../shaders/glsl/basic_texture_vertex.glsl | 35 + .../src/shaders/glsl/basic_vertex.glsl | 23 + .../src/shaders/glsl/ground_fragment.glsl | 40 + .../src/shaders/glsl/ground_vertex.glsl | 123 + .../src/shaders/glsl/sky_fragment.glsl | 41 + .../src/shaders/glsl/sky_vertex.glsl | 123 + .../src/shaders/glsl/star_field_fragment.glsl | 34 + .../src/shaders/glsl/star_field_vertex.glsl | 59 + .../shaders/glsl/surface_tile_fragment.glsl | 48 + .../src/shaders/glsl/surface_tile_vertex.glsl | 33 + .../WebWorldWind/src/shapes/AbstractMesh.js | 545 ++ .../WebWorldWind/src/shapes/AbstractShape.js | 429 ++ .../WebWorldWind/src/shapes/Annotation.js | 515 ++ .../src/shapes/AnnotationAttributes.js | 285 + web/test/WebWorldWind/src/shapes/Compass.js | 81 + .../WebWorldWind/src/shapes/GeographicMesh.js | 397 ++ .../WebWorldWind/src/shapes/GeographicText.js | 119 + web/test/WebWorldWind/src/shapes/Path.js | 624 ++ web/test/WebWorldWind/src/shapes/Placemark.js | 793 +++ .../src/shapes/PlacemarkAttributes.js | 243 + web/test/WebWorldWind/src/shapes/Polygon.js | 902 +++ .../WebWorldWind/src/shapes/ScreenImage.js | 374 ++ .../WebWorldWind/src/shapes/ScreenText.js | 87 + .../src/shapes/ShapeAttributes.js | 310 + .../WebWorldWind/src/shapes/SurfaceCircle.js | 183 + .../WebWorldWind/src/shapes/SurfaceEllipse.js | 230 + .../WebWorldWind/src/shapes/SurfaceImage.js | 179 + .../WebWorldWind/src/shapes/SurfacePolygon.js | 143 + .../src/shapes/SurfacePolyline.js | 125 + .../src/shapes/SurfaceRectangle.js | 198 + .../WebWorldWind/src/shapes/SurfaceSector.js | 139 + .../WebWorldWind/src/shapes/SurfaceShape.js | 1063 ++++ .../src/shapes/SurfaceShapeTile.js | 248 + .../src/shapes/SurfaceShapeTileBuilder.js | 512 ++ web/test/WebWorldWind/src/shapes/Text.js | 538 ++ .../WebWorldWind/src/shapes/TextAttributes.js | 225 + .../WebWorldWind/src/shapes/TriangleMesh.js | 384 ++ .../src/util/AbsentResourceList.js | 141 + web/test/WebWorldWind/src/util/Color.js | 353 ++ web/test/WebWorldWind/src/util/Font.js | 190 + .../WebWorldWind/src/util/FrameStatistics.js | 220 + .../WebWorldWind/src/util/GoToAnimator.js | 262 + web/test/WebWorldWind/src/util/HashMap.js | 91 + web/test/WebWorldWind/src/util/ImageSource.js | 60 + web/test/WebWorldWind/src/util/Insets.js | 135 + web/test/WebWorldWind/src/util/Level.js | 148 + web/test/WebWorldWind/src/util/LevelSet.js | 210 + web/test/WebWorldWind/src/util/Logger.js | 171 + web/test/WebWorldWind/src/util/Offset.js | 120 + .../WebWorldWind/src/util/PolygonSplitter.js | 452 ++ web/test/WebWorldWind/src/util/SunPosition.js | 166 + web/test/WebWorldWind/src/util/Tile.js | 570 ++ web/test/WebWorldWind/src/util/TileFactory.js | 50 + web/test/WebWorldWind/src/util/UrlBuilder.js | 46 + web/test/WebWorldWind/src/util/WWMath.js | 873 +++ web/test/WebWorldWind/src/util/WWUtil.js | 303 + .../WebWorldWind/src/util/WmsUrlBuilder.js | 182 + web/test/WebWorldWind/src/util/libtess.js | 5272 +++++++++++++++++ 152 files changed, 46891 insertions(+), 1 deletion(-) rename web/{assets => test/WebWorldWind}/images/powered-by-bing.png (100%) rename web/{assets => test/WebWorldWind}/images/stars.json (100%) rename web/{assets => test/WebWorldWind}/images/sunTexture.png (100%) create mode 100644 web/test/WebWorldWind/index.html create mode 100644 web/test/WebWorldWind/rollup.config.js create mode 100644 web/test/WebWorldWind/src/BasicWorldWindowController.js create mode 100644 web/test/WebWorldWind/src/README.md create mode 100644 web/test/WebWorldWind/src/WorldWind.js create mode 100644 web/test/WebWorldWind/src/WorldWindow.js create mode 100644 web/test/WebWorldWind/src/WorldWindowController.js create mode 100644 web/test/WebWorldWind/src/cache/GpuResourceCache.js create mode 100644 web/test/WebWorldWind/src/cache/MemoryCache.js create mode 100644 web/test/WebWorldWind/src/cache/MemoryCacheListener.js create mode 100644 web/test/WebWorldWind/src/error/AbstractError.js create mode 100644 web/test/WebWorldWind/src/error/ArgumentError.js create mode 100644 web/test/WebWorldWind/src/error/NotYetImplementedError.js create mode 100644 web/test/WebWorldWind/src/error/UnsupportedOperationError.js create mode 100644 web/test/WebWorldWind/src/geom/Angle.js create mode 100644 web/test/WebWorldWind/src/geom/BoundingBox.js create mode 100644 web/test/WebWorldWind/src/geom/Frustum.js create mode 100644 web/test/WebWorldWind/src/geom/Line.js create mode 100644 web/test/WebWorldWind/src/geom/Location.js create mode 100644 web/test/WebWorldWind/src/geom/Matrix.js create mode 100644 web/test/WebWorldWind/src/geom/Matrix3.js create mode 100644 web/test/WebWorldWind/src/geom/Plane.js create mode 100644 web/test/WebWorldWind/src/geom/Position.js create mode 100644 web/test/WebWorldWind/src/geom/Rectangle.js create mode 100644 web/test/WebWorldWind/src/geom/Sector.js create mode 100644 web/test/WebWorldWind/src/geom/Vec2.js create mode 100644 web/test/WebWorldWind/src/geom/Vec3.js create mode 100644 web/test/WebWorldWind/src/gesture/ClickRecognizer.js create mode 100644 web/test/WebWorldWind/src/gesture/DragRecognizer.js create mode 100644 web/test/WebWorldWind/src/gesture/GestureRecognizer.js create mode 100644 web/test/WebWorldWind/src/gesture/PanRecognizer.js create mode 100644 web/test/WebWorldWind/src/gesture/PinchRecognizer.js create mode 100644 web/test/WebWorldWind/src/gesture/RotationRecognizer.js create mode 100644 web/test/WebWorldWind/src/gesture/TapRecognizer.js create mode 100644 web/test/WebWorldWind/src/gesture/TiltRecognizer.js create mode 100644 web/test/WebWorldWind/src/gesture/Touch.js create mode 100644 web/test/WebWorldWind/src/globe/ArcgisElevationCoverage.js create mode 100644 web/test/WebWorldWind/src/globe/ArcgisElevationWorker.js create mode 100644 web/test/WebWorldWind/src/globe/AsterV2ElevationCoverage.js create mode 100644 web/test/WebWorldWind/src/globe/EarthElevationModel.js create mode 100644 web/test/WebWorldWind/src/globe/ElevationCoverage.js create mode 100644 web/test/WebWorldWind/src/globe/ElevationImage.js create mode 100644 web/test/WebWorldWind/src/globe/ElevationModel.js create mode 100644 web/test/WebWorldWind/src/globe/GebcoElevationCoverage.js create mode 100644 web/test/WebWorldWind/src/globe/Globe.js create mode 100644 web/test/WebWorldWind/src/globe/Terrain.js create mode 100644 web/test/WebWorldWind/src/globe/TerrainTile.js create mode 100644 web/test/WebWorldWind/src/globe/TerrainTileList.js create mode 100644 web/test/WebWorldWind/src/globe/Tessellator.js create mode 100644 web/test/WebWorldWind/src/globe/TiledElevationCoverage.js create mode 100644 web/test/WebWorldWind/src/globe/UsgsNedElevationCoverage.js create mode 100644 web/test/WebWorldWind/src/globe/UsgsNedHiElevationCoverage.js create mode 100644 web/test/WebWorldWind/src/layer/AtmosphereLayer.js create mode 100644 web/test/WebWorldWind/src/layer/Layer.js create mode 100644 web/test/WebWorldWind/src/layer/MercatorTiledImageLayer.js create mode 100644 web/test/WebWorldWind/src/layer/StarFieldLayer.js create mode 100644 web/test/WebWorldWind/src/layer/TiledImageLayer.js create mode 100644 web/test/WebWorldWind/src/layer/XYZLayer.js create mode 100644 web/test/WebWorldWind/src/navigate/LookAtNavigator.js create mode 100644 web/test/WebWorldWind/src/navigate/Navigator.js create mode 100644 web/test/WebWorldWind/src/pick/PickedObject.js create mode 100644 web/test/WebWorldWind/src/pick/PickedObjectList.js create mode 100644 web/test/WebWorldWind/src/projections/GeographicProjection.js create mode 100644 web/test/WebWorldWind/src/projections/ProjectionWgs84.js create mode 100644 web/test/WebWorldWind/src/render/DrawContext.js create mode 100644 web/test/WebWorldWind/src/render/FramebufferTexture.js create mode 100644 web/test/WebWorldWind/src/render/FramebufferTile.js create mode 100644 web/test/WebWorldWind/src/render/FramebufferTileController.js create mode 100644 web/test/WebWorldWind/src/render/ImageTile.js create mode 100644 web/test/WebWorldWind/src/render/OrderedRenderable.js create mode 100644 web/test/WebWorldWind/src/render/Renderable.js create mode 100644 web/test/WebWorldWind/src/render/ScreenCreditController.js create mode 100644 web/test/WebWorldWind/src/render/SurfaceRenderable.js create mode 100644 web/test/WebWorldWind/src/render/SurfaceTile.js create mode 100644 web/test/WebWorldWind/src/render/SurfaceTileRenderer.js create mode 100644 web/test/WebWorldWind/src/render/TextRenderer.js create mode 100644 web/test/WebWorldWind/src/render/Texture.js create mode 100644 web/test/WebWorldWind/src/render/TextureTile.js create mode 100644 web/test/WebWorldWind/src/shaders/AtmosphereProgram.js create mode 100644 web/test/WebWorldWind/src/shaders/BasicProgram.js create mode 100644 web/test/WebWorldWind/src/shaders/BasicTextureProgram.js create mode 100644 web/test/WebWorldWind/src/shaders/GpuProgram.js create mode 100644 web/test/WebWorldWind/src/shaders/GpuShader.js create mode 100644 web/test/WebWorldWind/src/shaders/GroundProgram.js create mode 100644 web/test/WebWorldWind/src/shaders/SkyProgram.js create mode 100644 web/test/WebWorldWind/src/shaders/StarFieldProgram.js create mode 100644 web/test/WebWorldWind/src/shaders/SurfaceTileRendererProgram.js create mode 100644 web/test/WebWorldWind/src/shaders/glsl/basic_fragment.glsl create mode 100644 web/test/WebWorldWind/src/shaders/glsl/basic_texture_fragment.glsl create mode 100644 web/test/WebWorldWind/src/shaders/glsl/basic_texture_vertex.glsl create mode 100644 web/test/WebWorldWind/src/shaders/glsl/basic_vertex.glsl create mode 100644 web/test/WebWorldWind/src/shaders/glsl/ground_fragment.glsl create mode 100644 web/test/WebWorldWind/src/shaders/glsl/ground_vertex.glsl create mode 100644 web/test/WebWorldWind/src/shaders/glsl/sky_fragment.glsl create mode 100644 web/test/WebWorldWind/src/shaders/glsl/sky_vertex.glsl create mode 100644 web/test/WebWorldWind/src/shaders/glsl/star_field_fragment.glsl create mode 100644 web/test/WebWorldWind/src/shaders/glsl/star_field_vertex.glsl create mode 100644 web/test/WebWorldWind/src/shaders/glsl/surface_tile_fragment.glsl create mode 100644 web/test/WebWorldWind/src/shaders/glsl/surface_tile_vertex.glsl create mode 100644 web/test/WebWorldWind/src/shapes/AbstractMesh.js create mode 100644 web/test/WebWorldWind/src/shapes/AbstractShape.js create mode 100644 web/test/WebWorldWind/src/shapes/Annotation.js create mode 100644 web/test/WebWorldWind/src/shapes/AnnotationAttributes.js create mode 100644 web/test/WebWorldWind/src/shapes/Compass.js create mode 100644 web/test/WebWorldWind/src/shapes/GeographicMesh.js create mode 100644 web/test/WebWorldWind/src/shapes/GeographicText.js create mode 100644 web/test/WebWorldWind/src/shapes/Path.js create mode 100644 web/test/WebWorldWind/src/shapes/Placemark.js create mode 100644 web/test/WebWorldWind/src/shapes/PlacemarkAttributes.js create mode 100644 web/test/WebWorldWind/src/shapes/Polygon.js create mode 100644 web/test/WebWorldWind/src/shapes/ScreenImage.js create mode 100644 web/test/WebWorldWind/src/shapes/ScreenText.js create mode 100644 web/test/WebWorldWind/src/shapes/ShapeAttributes.js create mode 100644 web/test/WebWorldWind/src/shapes/SurfaceCircle.js create mode 100644 web/test/WebWorldWind/src/shapes/SurfaceEllipse.js create mode 100644 web/test/WebWorldWind/src/shapes/SurfaceImage.js create mode 100644 web/test/WebWorldWind/src/shapes/SurfacePolygon.js create mode 100644 web/test/WebWorldWind/src/shapes/SurfacePolyline.js create mode 100644 web/test/WebWorldWind/src/shapes/SurfaceRectangle.js create mode 100644 web/test/WebWorldWind/src/shapes/SurfaceSector.js create mode 100644 web/test/WebWorldWind/src/shapes/SurfaceShape.js create mode 100644 web/test/WebWorldWind/src/shapes/SurfaceShapeTile.js create mode 100644 web/test/WebWorldWind/src/shapes/SurfaceShapeTileBuilder.js create mode 100644 web/test/WebWorldWind/src/shapes/Text.js create mode 100644 web/test/WebWorldWind/src/shapes/TextAttributes.js create mode 100644 web/test/WebWorldWind/src/shapes/TriangleMesh.js create mode 100644 web/test/WebWorldWind/src/util/AbsentResourceList.js create mode 100644 web/test/WebWorldWind/src/util/Color.js create mode 100644 web/test/WebWorldWind/src/util/Font.js create mode 100644 web/test/WebWorldWind/src/util/FrameStatistics.js create mode 100644 web/test/WebWorldWind/src/util/GoToAnimator.js create mode 100644 web/test/WebWorldWind/src/util/HashMap.js create mode 100644 web/test/WebWorldWind/src/util/ImageSource.js create mode 100644 web/test/WebWorldWind/src/util/Insets.js create mode 100644 web/test/WebWorldWind/src/util/Level.js create mode 100644 web/test/WebWorldWind/src/util/LevelSet.js create mode 100644 web/test/WebWorldWind/src/util/Logger.js create mode 100644 web/test/WebWorldWind/src/util/Offset.js create mode 100644 web/test/WebWorldWind/src/util/PolygonSplitter.js create mode 100644 web/test/WebWorldWind/src/util/SunPosition.js create mode 100644 web/test/WebWorldWind/src/util/Tile.js create mode 100644 web/test/WebWorldWind/src/util/TileFactory.js create mode 100644 web/test/WebWorldWind/src/util/UrlBuilder.js create mode 100644 web/test/WebWorldWind/src/util/WWMath.js create mode 100644 web/test/WebWorldWind/src/util/WWUtil.js create mode 100644 web/test/WebWorldWind/src/util/WmsUrlBuilder.js create mode 100644 web/test/WebWorldWind/src/util/libtess.js diff --git a/package-lock.json b/package-lock.json index 1ef18acc..ec5c9a7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -591,6 +591,16 @@ "color-convert": "^1.9.0" } }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npm.taobao.org/anymatch/download/anymatch-3.1.1.tgz", + "integrity": "sha1-xV7PAhheJGklk5kxDBc84xIzsUI=", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npm.taobao.org/argparse/download/argparse-1.0.10.tgz", @@ -738,6 +748,12 @@ "integrity": "sha1-ZfCvOC9Xi83HQr2cKB6cstd2gyg=", "dev": true }, + "binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npm.taobao.org/binary-extensions/download/binary-extensions-2.1.0.tgz", + "integrity": "sha1-MPpAyef+B9vIlWeM0ocCTeokHdk=", + "dev": true + }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npm.taobao.org/bluebird/download/bluebird-3.7.2.tgz?cache=0&sync_timestamp=1586263933818&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbluebird%2Fdownload%2Fbluebird-3.7.2.tgz", @@ -767,6 +783,15 @@ "concat-map": "0.0.1" } }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npm.taobao.org/braces/download/braces-3.0.2.tgz", + "integrity": "sha1-NFThpGLujVmeI23zNs2epPiv4Qc=", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, "browserslist": { "version": "4.12.0", "resolved": "https://registry.npm.taobao.org/browserslist/download/browserslist-4.12.0.tgz?cache=0&sync_timestamp=1587419799867&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbrowserslist%2Fdownload%2Fbrowserslist-4.12.0.tgz", @@ -884,6 +909,22 @@ "integrity": "sha1-kAlISfCTfy7twkJdDSip5fDLrZ4=", "dev": true }, + "chokidar": { + "version": "3.4.2", + "resolved": "https://registry.npm.taobao.org/chokidar/download/chokidar-3.4.2.tgz", + "integrity": "sha1-ONyOZY3sOAl0HrPve7Ckf+QkIy0=", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.4.0" + } + }, "chromium-pickle-js": { "version": "0.2.0", "resolved": "https://registry.npm.taobao.org/chromium-pickle-js/download/chromium-pickle-js-0.2.0.tgz", @@ -2123,6 +2164,15 @@ "flat-cache": "^2.0.1" } }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npm.taobao.org/fill-range/download/fill-range-7.0.1.tgz", + "integrity": "sha1-GRmmp8df44ssfHflGYU12prN2kA=", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npm.taobao.org/find-up/download/find-up-2.1.0.tgz", @@ -2205,6 +2255,13 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npm.taobao.org/fsevents/download/fsevents-2.1.3.tgz", + "integrity": "sha1-+3OHA66NL5/pAMM4Nt3r7ouX8j4=", + "dev": true, + "optional": true + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npm.taobao.org/function-bind/download/function-bind-1.1.1.tgz", @@ -2733,6 +2790,15 @@ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", "dev": true }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npm.taobao.org/is-binary-path/download/is-binary-path-2.1.0.tgz", + "integrity": "sha1-6h9/O4DwZCNug0cPhsCcJU+0Wwk=", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, "is-callable": { "version": "1.1.4", "resolved": "https://registry.npm.taobao.org/is-callable/download/is-callable-1.1.4.tgz", @@ -2792,6 +2858,12 @@ "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", "dev": true }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npm.taobao.org/is-number/download/is-number-7.0.0.tgz", + "integrity": "sha1-dTU0W4lnNNX4DE0GxQlVUnoU8Ss=", + "dev": true + }, "is-obj": { "version": "2.0.0", "resolved": "https://registry.npm.taobao.org/is-obj/download/is-obj-2.0.0.tgz", @@ -3116,6 +3188,12 @@ "integrity": "sha1-aZs8OKxvHXKAkaZGULZdOIUC/Vs=", "dev": true }, + "memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npm.taobao.org/memorystream/download/memorystream-0.3.1.tgz", + "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", + "dev": true + }, "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npm.taobao.org/mimic-fn/download/mimic-fn-2.1.0.tgz?cache=0&sync_timestamp=1560442058146&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmimic-fn%2Fdownload%2Fmimic-fn-2.1.0.tgz", @@ -3202,6 +3280,12 @@ "validate-npm-package-license": "^3.0.1" } }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npm.taobao.org/normalize-path/download/normalize-path-3.0.0.tgz", + "integrity": "sha1-Dc1p/yOhybEf0JeDFmRKA4ghamU=", + "dev": true + }, "normalize-url": { "version": "3.3.0", "resolved": "https://registry.npm.taobao.org/normalize-url/download/normalize-url-3.3.0.tgz", @@ -3228,6 +3312,63 @@ } } }, + "npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npm.taobao.org/npm-run-all/download/npm-run-all-4.1.5.tgz", + "integrity": "sha1-BEdiAqFe4OLiFAgIYb/xKlHZj7o=", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "dependencies": { + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npm.taobao.org/load-json-file/download/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npm.taobao.org/path-type/download/path-type-3.0.0.tgz", + "integrity": "sha1-zvMdyOCho7sNEFwM2Xzzv0f0428=", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npm.taobao.org/pify/download/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npm.taobao.org/read-pkg/download/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + } + } + }, "nth-check": { "version": "1.0.2", "resolved": "https://registry.npm.taobao.org/nth-check/download/nth-check-1.0.2.tgz", @@ -3669,6 +3810,18 @@ "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", "dev": true }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npm.taobao.org/picomatch/download/picomatch-2.2.2.tgz", + "integrity": "sha1-IfMz6ba46v8CRo9RRupAbTRfTa0=", + "dev": true + }, + "pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npm.taobao.org/pidtree/download/pidtree-0.3.1.tgz", + "integrity": "sha1-7wmsLMBTPfHzJQzPLE02aw0SEUo=", + "dev": true + }, "pify": { "version": "5.0.0", "resolved": "https://registry.npm.taobao.org/pify/download/pify-5.0.0.tgz", @@ -4477,6 +4630,15 @@ "util-deprecate": "~1.0.1" } }, + "readdirp": { + "version": "3.4.0", + "resolved": "https://registry.npm.taobao.org/readdirp/download/readdirp-3.4.0.tgz", + "integrity": "sha1-n9zN+ekVWAVEkiGsZF6DA6tbmto=", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npm.taobao.org/regenerate/download/regenerate-1.4.0.tgz", @@ -5010,6 +5172,12 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, + "shell-quote": { + "version": "1.7.2", + "resolved": "https://registry.npm.taobao.org/shell-quote/download/shell-quote-1.7.2.tgz", + "integrity": "sha1-Z6fQLHbJ2iT5nSCAj8re0ODgS+I=", + "dev": true + }, "side-channel": { "version": "1.0.2", "resolved": "https://registry.npm.taobao.org/side-channel/download/side-channel-1.0.2.tgz", @@ -5272,6 +5440,69 @@ } } }, + "string.prototype.padend": { + "version": "3.1.0", + "resolved": "https://registry.npm.taobao.org/string.prototype.padend/download/string.prototype.padend-3.1.0.tgz?cache=0&sync_timestamp=1576312157572&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fstring.prototype.padend%2Fdownload%2Fstring.prototype.padend-3.1.0.tgz", + "integrity": "sha1-3Aj1eoAQ3FwVNVAxj2fhOtu3KsM=", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npm.taobao.org/es-abstract/download/es-abstract-1.17.6.tgz", + "integrity": "sha1-kUIHFweFeyysx7iey2cDFsPi1So=", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npm.taobao.org/es-to-primitive/download/es-to-primitive-1.2.1.tgz", + "integrity": "sha1-5VzUyc3BiLzvsDs2bHNjI/xciYo=", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npm.taobao.org/has-symbols/download/has-symbols-1.0.1.tgz", + "integrity": "sha1-n1IUdYpEGWxAbZvXbOv4HsLdMeg=", + "dev": true + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npm.taobao.org/is-callable/download/is-callable-1.2.0.tgz", + "integrity": "sha1-gzNlYLVKOONeOi33r9BFTWkUaLs=", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npm.taobao.org/is-regex/download/is-regex-1.1.1.tgz?cache=0&sync_timestamp=1596555700840&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fis-regex%2Fdownload%2Fis-regex-1.1.1.tgz", + "integrity": "sha1-xvmKrMVG9s7FRooHt7FTq1ZKV7k=", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + } + } + }, "string.prototype.trimend": { "version": "1.0.1", "resolved": "https://registry.npm.taobao.org/string.prototype.trimend/download/string.prototype.trimend-1.0.1.tgz", @@ -5805,6 +6036,15 @@ "integrity": "sha1-zgqgwvPfat+FLvtASng+d8BHV3E=", "dev": true }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npm.taobao.org/to-regex-range/download/to-regex-range-5.0.1.tgz", + "integrity": "sha1-FkjESq58jZiKMmAY7XL1tN0DkuQ=", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, "truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npm.taobao.org/truncate-utf8-bytes/download/truncate-utf8-bytes-1.0.2.tgz", diff --git a/package.json b/package.json index c6255f62..542c8706 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "eslint": "cd web && eslint src --fix", "clean": "node scripts/clean.js", "test": "echo \"Error: no test specified\" && exit 1", - "clear": "npm prune" + "clear": "npm prune", + "build-wind": "cd web/test/WebWorldWind && rollup -c rollup.config.js", + "dev-wind": "cd web/test/WebWorldWind && rollup -c rollup.config.js --watch" }, "dependencies": { "@tweenjs/tween.js": "^18.5.0", diff --git a/web/assets/images/powered-by-bing.png b/web/test/WebWorldWind/images/powered-by-bing.png similarity index 100% rename from web/assets/images/powered-by-bing.png rename to web/test/WebWorldWind/images/powered-by-bing.png diff --git a/web/assets/images/stars.json b/web/test/WebWorldWind/images/stars.json similarity index 100% rename from web/assets/images/stars.json rename to web/test/WebWorldWind/images/stars.json diff --git a/web/assets/images/sunTexture.png b/web/test/WebWorldWind/images/sunTexture.png similarity index 100% rename from web/assets/images/sunTexture.png rename to web/test/WebWorldWind/images/sunTexture.png diff --git a/web/test/WebWorldWind/index.html b/web/test/WebWorldWind/index.html new file mode 100644 index 00000000..9ba041c3 --- /dev/null +++ b/web/test/WebWorldWind/index.html @@ -0,0 +1,37 @@ + + + + +
+ +gpuCacheSize: A Number indicating the size in bytes to allocate from GPU memory for
+ * resources such as textures, GLSL programs and buffer objects. Default is 250e6 (250 MB).baseUrl: The URL of the directory containing the WorldWind Library and its resources.layerRetrievalQueueSize: The number of concurrent tile requests allowed per layer. The default is 16.coverageRetrievalQueueSize: The number of concurrent tile requests allowed per elevation coverage. The default is 16.bingLogoPlacement: An {@link Offset} to place a Bing logo attribution. The default is a 7px margin inset from the lower right corner of the screen.bingLogoAlignment: An {@link Offset} to align the Bing logo relative to its placement position. The default is the lower right corner of the logo.redrawCallback(worldWindow, stage);.
+ * The stage will be either WorldWind.BEFORE_REDRAW or WorldWind.AFTER_REDRAW indicating whether the
+ * callback has been called either immediately before or immediately after a redraw, respectively.
+ * Applications may add functions to this array or remove them.
+ * @type {Function[]}
+ * @readonly
+ * @memberof WorldWindow.prototype
+ */
+ redrawCallbacks: {
+ get: function () {
+ return this._redrawCallbacks;
+ }
+ }
+});
+
+/**
+ * Converts window coordinates to coordinates relative to this WorldWindow's canvas.
+ * @param {Number} x The X coordinate to convert.
+ * @param {Number} y The Y coordinate to convert.
+ * @returns {Vec2} The converted coordinates.
+ */
+WorldWindow.prototype.canvasCoordinates = function (x, y) {
+ var bbox = this.canvas.getBoundingClientRect(),
+ xc = x - (bbox.left + this.canvas.clientLeft),// * (this.canvas.width / bbox.width),
+ yc = y - (bbox.top + this.canvas.clientTop);// * (this.canvas.height / bbox.height);
+
+ return new Vec2(xc, yc);
+};
+
+WorldWindow.prototype.onGestureEvent = function (event) {
+ this.worldWindowController.onGestureEvent(event);
+};
+
+/**
+ * Registers an event listener for the specified event type on this WorldWindow's canvas. This function
+ * delegates the processing of events to the WorldWindow's canvas. For details on this function and its
+ * arguments, see the W3C [EventTarget]{@link https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget}
+ * documentation.
+ *
+ * Registering event listeners using this function enables applications to prevent the WorldWindow's default
+ * navigation behavior. To prevent default navigation behavior, call the [Event]{@link https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-Event}'s
+ * preventDefault method from within an event listener for any events the navigator should not respond to.
+ *
+ * When an event occurs, this calls the registered event listeners in order of reverse registration. Since the
+ * WorldWindow registers its navigator event listeners first, application event listeners are called before
+ * navigator event listeners.
+ *
+ * @param type The event type to listen for.
+ * @param listener The function to call when the event occurs.
+ * @throws {ArgumentError} If any argument is null or undefined.
+ */
+WorldWindow.prototype.addEventListener = function (type, listener) {
+ if (!type) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "addEventListener", "missingType"));
+ }
+
+ if (!listener) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "addEventListener", "missingListener"));
+ }
+
+ var thisWorldWindow = this;
+ var entry = this.eventListeners[type];
+ if (!entry) {
+ entry = {
+ listeners: [],
+ callback: function (event) { // calls listeners in reverse registration order
+ event.worldWindow = thisWorldWindow;
+ for (var i = 0, len = entry.listeners.length; i < len; i++) {
+ entry.listeners[i](event);
+ }
+ }
+ };
+ this.eventListeners[type] = entry;
+ }
+
+ var index = entry.listeners.indexOf(listener);
+ if (index == -1) { // suppress duplicate listeners
+ entry.listeners.splice(0, 0, listener); // insert the listener at the beginning of the list
+
+ if (entry.listeners.length == 1) { // first listener added, add the event listener callback
+ this.canvas.addEventListener(type, entry.callback, false);
+ }
+ }
+};
+
+/**
+ * Removes an event listener for the specified event type from this WorldWindow's canvas. The listener must be
+ * the same object passed to addEventListener. Calling removeEventListener with arguments that do not identify a
+ * currently registered listener has no effect.
+ *
+ * @param type Indicates the event type the listener registered for.
+ * @param listener The listener to remove. Must be the same function object passed to addEventListener.
+ * @throws {ArgumentError} If any argument is null or undefined.
+ */
+WorldWindow.prototype.removeEventListener = function (type, listener) {
+ if (!type) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "removeEventListener", "missingType"));
+ }
+
+ if (!listener) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "removeEventListener", "missingListener"));
+ }
+
+ var entry = this.eventListeners[type];
+ if (!entry) {
+ return; // no entry for the specified type
+ }
+
+ var index = entry.listeners.indexOf(listener);
+ if (index != -1) {
+ entry.listeners.splice(index, 1); // remove the listener from the list
+
+ if (entry.listeners.length == 0) { // last listener removed, remove the event listener callback
+ this.canvas.removeEventListener(type, entry.callback, false);
+ }
+ }
+};
+
+/**
+ * Causes this WorldWindow to redraw itself at the next available opportunity. The redraw occurs on the main
+ * thread at a time of the browser's discretion. Applications should call redraw after changing the World
+ * Window's state, but should not expect that change to be reflected on screen immediately after this function
+ * returns. This is the preferred method for requesting a redraw of the WorldWindow.
+ */
+WorldWindow.prototype.redraw = function () {
+ this.redrawRequested = true; // redraw during the next animation frame
+};
+
+/**
+ * Requests the WorldWind objects displayed at a specified screen-coordinate point.
+ *
+ * If the point intersects the terrain, the returned list contains an object identifying the associated geographic
+ * position. This returns an empty list when nothing in the WorldWind scene intersects the specified point.
+ *
+ * @param pickPoint The point to examine in this WorldWindow's screen coordinates.
+ * @returns {PickedObjectList} A list of picked WorldWind objects at the specified pick point.
+ * @throws {ArgumentError} If the specified pick point is null or undefined.
+ */
+WorldWindow.prototype.pick = function (pickPoint) {
+ if (!pickPoint) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "pick", "missingPoint"));
+ }
+
+ // Suppress the picking operation and return an empty list when the WebGL context has been lost.
+ if (this.drawContext.currentGlContext.isContextLost()) {
+ return new PickedObjectList();
+ }
+
+ this.resize();
+ this.resetDrawContext();
+ this.drawContext.pickingMode = true;
+ this.drawContext.pickPoint = pickPoint;
+ this.drawContext.pickRay = this.rayThroughScreenPoint(pickPoint);
+ this.drawFrame();
+
+ return this.drawContext.objectsAtPickPoint;
+};
+
+/**
+ * Requests the position of the WorldWind terrain at a specified screen-coordinate point. If the point
+ * intersects the terrain, the returned list contains a single object identifying the associated geographic
+ * position. Otherwise this returns an empty list.
+ * @param pickPoint The point to examine in this WorldWindow's screen coordinates.
+ * @returns {PickedObjectList} A list containing the picked WorldWind terrain position at the specified point,
+ * or an empty list if the point does not intersect the terrain.
+ * @throws {ArgumentError} If the specified pick point is null or undefined.
+ */
+WorldWindow.prototype.pickTerrain = function (pickPoint) {
+ if (!pickPoint) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "pickTerrain", "missingPoint"));
+ }
+
+ // Suppress the picking operation and return an empty list when the WebGL context has been lost.
+ if (this.drawContext.currentGlContext.isContextLost()) {
+ return new PickedObjectList();
+ }
+
+ this.resize();
+ this.resetDrawContext();
+ this.drawContext.pickingMode = true;
+ this.drawContext.pickTerrainOnly = true;
+ this.drawContext.pickPoint = pickPoint;
+ this.drawContext.pickRay = this.rayThroughScreenPoint(pickPoint);
+ this.drawFrame();
+
+ return this.drawContext.objectsAtPickPoint;
+};
+
+/**
+ * Requests the WorldWind objects displayed within a specified screen-coordinate region. This returns all
+ * objects that intersect the specified region, regardless of whether or not an object is actually visible, and
+ * marks objects that are visible as on top.
+ * @param {Rectangle} rectangle The screen coordinate rectangle identifying the region to search.
+ * @returns {PickedObjectList} A list of visible WorldWind objects within the specified region.
+ * @throws {ArgumentError} If the specified rectangle is null or undefined.
+ */
+WorldWindow.prototype.pickShapesInRegion = function (rectangle) {
+ if (!rectangle) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "pickShapesInRegion", "missingRectangle"));
+ }
+
+ // Suppress the picking operation and return an empty list when the WebGL context has been lost.
+ if (this.drawContext.currentGlContext.isContextLost()) {
+ return new PickedObjectList();
+ }
+
+ this.resize();
+ this.resetDrawContext();
+ this.drawContext.pickingMode = true;
+ this.drawContext.regionPicking = true;
+ this.drawContext.pickRectangle =
+ new Rectangle(rectangle.x, this.canvas.height - rectangle.y, rectangle.width, rectangle.height);
+ this.drawFrame();
+
+ return this.drawContext.objectsAtPickPoint;
+};
+
+// Internal function. Intentionally not documented.
+WorldWindow.prototype.createContext = function (canvas) {
+ // Request a WebGL context with antialiasing is disabled. Antialiasing causes gaps to appear at the edges of
+ // terrain tiles.
+ var glAttrs = { antialias: false, stencil: true },
+ gl = canvas.getContext("webgl", glAttrs);
+ if (!gl) {
+ gl = canvas.getContext("experimental-webgl", glAttrs);
+ }
+
+ // uncomment to debug WebGL
+ //var gl = WebGLDebugUtils.makeDebugContext(this.canvas.getContext("webgl"),
+ // this.throwOnGLError,
+ // this.logAndValidate
+ //);
+
+ return gl;
+};
+
+// Internal function. Intentionally not documented.
+WorldWindow.prototype.handleContextLost = function (event) {
+ Logger.log(Logger.LEVEL_INFO, "WebGL context event: " + event.statusMessage);
+ // Inform WebGL that we handle context restoration, enabling the context restored event to be delivered.
+ event.preventDefault();
+ // Notify the draw context that the WebGL rendering context has been lost.
+ this.drawContext.contextLost();
+ // Stop the rendering animation frame loop, resuming only if the WebGL context is restored.
+ window.cancelAnimationFrame(this.redrawRequestId);
+};
+
+// Internal function. Intentionally not documented.
+WorldWindow.prototype.handleContextRestored = function (event) {
+ Logger.log(Logger.LEVEL_INFO, "WebGL context event: " + event.statusMessage);
+ // Notify the draw context that the WebGL rendering context has been restored.
+ this.drawContext.contextRestored();
+ // Resume the rendering animation frame loop until the WebGL context is lost.
+ this.redraw();
+ this.animationFrameLoop();
+};
+
+// Internal function. Intentionally not documented.
+WorldWindow.prototype.handleRedrawEvent = function (event) {
+ this.redraw(); // redraw in the next animation frame
+};
+
+// Internal function. Intentionally not documented.
+WorldWindow.prototype.animationFrameLoop = function () {
+ // Render to the WebGL context as needed.
+ this.redrawIfNeeded();
+
+ // Continue the animation frame loop until the WebGL context is lost.
+ var thisWindow = this;
+
+ function animationFrameCallback() {
+ thisWindow.animationFrameLoop();
+ }
+
+ this.redrawRequestId = window.requestAnimationFrame(animationFrameCallback);
+};
+
+// Internal function. Intentionally not documented.
+WorldWindow.prototype.redrawIfNeeded = function () {
+ // Check if the drawing buffer needs to resize to match its screen size, which requires a redraw.
+ this.resize();
+
+ // Redraw the WebGL drawing buffer only when necessary.
+ if (!this.redrawRequested) {
+ return;
+ }
+
+ try {
+ // Prepare to redraw and notify the redraw callbacks that a redraw is about to occur.
+ this.redrawRequested = false;
+ this.drawContext.previousRedrawTimestamp = this.drawContext.timestamp;
+ this.callRedrawCallbacks(WorldWind.BEFORE_REDRAW);
+ // Redraw the WebGL drawing buffer.
+ this.resetDrawContext();
+ this.drawFrame();
+ } catch (e) {
+ Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "redrawIfNeeded",
+ "Exception occurred during redrawing.\n" + e.toString());
+ } finally {
+ // Notify the redraw callbacks that a redraw has completed.
+ this.callRedrawCallbacks(WorldWind.AFTER_REDRAW);
+ // Handle rendering code redraw requests.
+ if (this.drawContext.redrawRequested) {
+ this.redrawRequested = true;
+ }
+ }
+};
+
+// Internal function. Intentionally not documented.
+WorldWindow.prototype.resize = function () {
+ var gl = this.drawContext.currentGlContext,
+ width = gl.canvas.clientWidth * this.pixelScale,
+ height = gl.canvas.clientHeight * this.pixelScale;
+
+ if (gl.canvas.width != width ||
+ gl.canvas.height != height) {
+
+ // Make the canvas drawing buffer size match its screen size.
+ gl.canvas.width = width;
+ gl.canvas.height = height;
+
+ // Set the WebGL viewport to match the canvas drawing buffer size.
+ gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
+ this.viewport = new Rectangle(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
+
+ // Cause this WorldWindow to redraw with the new size.
+ this.redrawRequested = true;
+ }
+};
+
+// Internal. Intentionally not documented.
+WorldWindow.prototype.computeViewingTransform = function (projection, modelview) {
+ if (!modelview) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "computeViewingTransform", "missingModelview"));
+ }
+
+ modelview.setToIdentity();
+ this.worldWindowController.applyLimits();
+ var globe = this.globe;
+ var navigator = this.navigator;
+ var lookAtPosition = new Position(navigator.lookAtLocation.latitude, navigator.lookAtLocation.longitude, 0);
+ modelview.multiplyByLookAtModelview(lookAtPosition, navigator.range, navigator.heading, navigator.tilt, navigator.roll, globe);
+
+ if (projection) {
+ projection.setToIdentity();
+ var globeRadius = WWMath.max(globe.equatorialRadius, globe.polarRadius),
+ eyePoint = modelview.extractEyePoint(new Vec3(0, 0, 0)),
+ eyePos = globe.computePositionFromPoint(eyePoint[0], eyePoint[1], eyePoint[2], new Position(0, 0, 0)),
+ eyeHorizon = WWMath.horizonDistanceForGlobeRadius(globeRadius, eyePos.altitude),
+ atmosphereHorizon = WWMath.horizonDistanceForGlobeRadius(globeRadius, 160000),
+ viewport = this.viewport;
+
+ // Set the far clip distance to the smallest value that does not clip the atmosphere.
+ // TODO adjust the clip plane distances based on the navigator's orientation - shorter distances when the
+ // TODO horizon is not in view
+ // TODO parameterize the object altitude for horizon distance
+ var farDistance = eyeHorizon + atmosphereHorizon;
+ if (farDistance < 1e3)
+ farDistance = 1e3;
+
+ // Compute the near clip distance in order to achieve a desired depth resolution at the far clip distance.
+ // This computed distance is limited such that it does not intersect the terrain when possible and is never
+ // less than a predetermined minimum (usually one). The computed near distance automatically scales with the
+ // resolution of the WebGL depth buffer.
+ var nearDistance = WWMath.perspectiveNearDistanceForFarDistance(farDistance, 10, this.depthBits);
+
+ // Prevent the near clip plane from intersecting the terrain.
+ var distanceToSurface = eyePos.altitude - globe.elevationAtLocation(eyePos.latitude, eyePos.longitude);
+ if (distanceToSurface > 0) {
+ var maxNearDistance = WWMath.perspectiveNearDistance(viewport.width, viewport.height, distanceToSurface);
+ if (nearDistance > maxNearDistance) {
+ nearDistance = maxNearDistance;
+ }
+ }
+
+ if (nearDistance < 1) {
+ nearDistance = 1;
+ }
+
+ // Compute the current projection matrix based on this navigator's perspective properties and the current
+ // WebGL viewport.
+ projection.setToPerspectiveProjection(viewport.width, viewport.height, nearDistance, farDistance);
+ }
+};
+
+// Internal. Intentionally not documented.
+WorldWindow.prototype.computePixelMetrics = function (projection) {
+ var projectionInv = Matrix.fromIdentity();
+ projectionInv.invertMatrix(projection);
+
+ // Compute the eye coordinate rectangles carved out of the frustum by the near and far clipping planes, and
+ // the distance between those planes and the eye point along the -Z axis. The rectangles are determined by
+ // transforming the bottom-left and top-right points of the frustum from clip coordinates to eye
+ // coordinates.
+ var nbl = new Vec3(-1, -1, -1),
+ ntr = new Vec3(+1, +1, -1),
+ fbl = new Vec3(-1, -1, +1),
+ ftr = new Vec3(+1, +1, +1);
+ // Convert each frustum corner from clip coordinates to eye coordinates by multiplying by the inverse
+ // projection matrix.
+ nbl.multiplyByMatrix(projectionInv);
+ ntr.multiplyByMatrix(projectionInv);
+ fbl.multiplyByMatrix(projectionInv);
+ ftr.multiplyByMatrix(projectionInv);
+
+ var nrRectWidth = WWMath.fabs(ntr[0] - nbl[0]),
+ frRectWidth = WWMath.fabs(ftr[0] - fbl[0]),
+ nrDistance = -nbl[2],
+ frDistance = -fbl[2];
+
+ // Compute the scale and offset used to determine the width of a pixel on a rectangle carved out of the
+ // frustum at a distance along the -Z axis in eye coordinates. These values are found by computing the scale
+ // and offset of a frustum rectangle at a given distance, then dividing each by the viewport width.
+ var frustumWidthScale = (frRectWidth - nrRectWidth) / (frDistance - nrDistance),
+ frustumWidthOffset = nrRectWidth - frustumWidthScale * nrDistance;
+
+ return {
+ pixelSizeFactor: frustumWidthScale / this.viewport.width,
+ pixelSizeOffset: frustumWidthOffset / this.viewport.height
+ };
+};
+
+/**
+ * Computes the approximate size of a pixel at a specified distance from the eye point.
+ * + * This method assumes rectangular pixels, where pixel coordinates denote + * infinitely thin spaces between pixels. The units of the returned size are in model coordinates per pixel + * (usually meters per pixel). This returns 0 if the specified distance is zero. The returned size is undefined + * if the distance is less than zero. + * + * @param {Number} distance The distance from the eye point at which to determine pixel size, in model + * coordinates. + * @returns {Number} The approximate pixel size at the specified distance from the eye point, in model + * coordinates per pixel. + */ +WorldWindow.prototype.pixelSizeAtDistance = function (distance) { + this.computeViewingTransform(this.scratchProjection, this.scratchModelview); + var pixelMetrics = this.computePixelMetrics(this.scratchProjection); + return pixelMetrics.pixelSizeFactor * distance + pixelMetrics.pixelSizeOffset; +}; + +// Internal. Intentionally not documented. +WorldWindow.prototype.computeDrawContext = function () { + var dc = this.drawContext; + + this.computeViewingTransform(dc.projection, dc.modelview); + dc.viewport = this.viewport; + dc.eyePoint = dc.modelview.extractEyePoint(new Vec3(0, 0, 0)); + + dc.modelviewProjection.setToIdentity(); + dc.modelviewProjection.setToMultiply(dc.projection, dc.modelview); + + var pixelMetrics = this.computePixelMetrics(dc.projection); + dc.pixelSizeFactor = pixelMetrics.pixelSizeFactor; + dc.pixelSizeOffset = pixelMetrics.pixelSizeOffset; + + // Compute the inverse of the modelview, projection, and modelview-projection matrices. The inverse matrices + // are used to support operations on navigator state. + var modelviewInv = Matrix.fromIdentity(); + modelviewInv.invertOrthonormalMatrix(dc.modelview); + + dc.modelviewNormalTransform = Matrix.fromIdentity().setToTransposeOfMatrix(modelviewInv.upper3By3()); + + // Compute the frustum in model coordinates. Start by computing the frustum in eye coordinates from the + // projection matrix, then transform this frustum to model coordinates by multiplying its planes by the + // transpose of the modelview matrix. We use the transpose of the modelview matrix because planes are + // transformed by the inverse transpose of a matrix, and we want to transform from eye coordinates to model + // coordinates. + var modelviewTranspose = Matrix.fromIdentity(); + modelviewTranspose.setToTransposeOfMatrix(dc.modelview); + dc.frustumInModelCoordinates = Frustum.fromProjectionMatrix(dc.projection); + dc.frustumInModelCoordinates.transformByMatrix(modelviewTranspose); + dc.frustumInModelCoordinates.normalize(); +}; + +// Internal. Intentionally not documented. +WorldWindow.prototype.resetDrawContext = function () { + this.globe.offset = 0; + + var dc = this.drawContext; + dc.reset(); + dc.globe = this.globe; + dc.navigator = this.navigator; + dc.layers = this.layers.slice(); + dc.layers.push(dc.screenCreditController); + this.computeDrawContext(); + dc.verticalExaggeration = this.verticalExaggeration; + dc.surfaceOpacity = this.surfaceOpacity; + dc.deepPicking = this.deepPicking; + dc.frameStatistics = this.frameStatistics; + dc.pixelScale = this.pixelScale; + dc.update(); +}; + +/* useful stuff to debug WebGL */ +/* + function logGLCall(functionName, args) { + console.log("gl." + functionName + "(" + + WebGLDebugUtils.glFunctionArgsToString(functionName, args) + ")"); + }; + + function validateNoneOfTheArgsAreUndefined(functionName, args) { + for (var ii = 0; ii < args.length; ++ii) { + if (args[ii] === undefined) { + console.error("undefined passed to gl." + functionName + "(" + + WebGLDebugUtils.glFunctionArgsToString(functionName, args) + ")"); + } + } + }; + + WorldWindow.prototype.logAndValidate = function logAndValidate(functionName, args) { + logGLCall(functionName, args); + validateNoneOfTheArgsAreUndefined (functionName, args); + }; + + WorldWindow.prototype.throwOnGLError = function throwOnGLError(err, funcName, args) { + throw WebGLDebugUtils.glEnumToString(err) + " was caused by call to: " + funcName; + }; + */ + +// Internal function. Intentionally not documented. +WorldWindow.prototype.drawFrame = function () { + try { + this.drawContext.frameStatistics.beginFrame(); + this.beginFrame(); + + if (this.drawContext.globe.is2D() && this.drawContext.globe.continuous) { + this.do2DContiguousRepaint(); + } else { + this.doNormalRepaint(); + } + + } finally { + this.endFrame(); + this.drawContext.frameStatistics.endFrame(); + //console.log(this.drawContext.frameStatistics.frameTime); + } +}; + +WorldWindow.prototype.doNormalRepaint = function () { + this.createTerrain(); + this.clearFrame(); + this.deferOrderedRendering = false; + if (this.drawContext.pickingMode) { + if (this.drawContext.makePickFrustum()) { + this.doPick(); + this.resolvePick(); + } + } else { + this.doDraw(); + if (this.subsurfaceMode && this.hasStencilBuffer) { + this.redrawSurface(); + this.drawScreenRenderables(); + } + } +}; + +WorldWindow.prototype.do2DContiguousRepaint = function () { + this.createTerrain2DContiguous(); + this.clearFrame(); + if (this.drawContext.pickingMode) { + if (this.drawContext.makePickFrustum()) { + this.pick2DContiguous(); + this.resolvePick(); + } + } else { + this.draw2DContiguous(); + } +}; + +WorldWindow.prototype.resolvePick = function () { + if (this.drawContext.pickTerrainOnly) { + this.resolveTerrainPick(); + } else if (this.drawContext.regionPicking) { + this.resolveRegionPick(); + } else { + this.resolveTopPick(); + } +}; + +// Internal function. Intentionally not documented. +WorldWindow.prototype.beginFrame = function () { + var gl = this.drawContext.currentGlContext; + gl.enable(gl.BLEND); + gl.enable(gl.CULL_FACE); + gl.enable(gl.DEPTH_TEST); + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + gl.depthFunc(gl.LEQUAL); + + if (this.drawContext.pickingMode) { + this.drawContext.makePickFramebuffer(); + this.drawContext.bindFramebuffer(this.drawContext.pickFramebuffer); + } +}; + +// Internal function. Intentionally not documented. +WorldWindow.prototype.endFrame = function () { + var gl = this.drawContext.currentGlContext; + gl.disable(gl.BLEND); + gl.disable(gl.CULL_FACE); + gl.disable(gl.DEPTH_TEST); + gl.blendFunc(gl.ONE, gl.ZERO); + gl.depthFunc(gl.LESS); + gl.clearColor(0, 0, 0, 1); + + this.drawContext.bindFramebuffer(null); + this.drawContext.bindProgram(null); +}; + +// Internal function. Intentionally not documented. +WorldWindow.prototype.clearFrame = function () { + var dc = this.drawContext, + gl = dc.currentGlContext; + + gl.clearColor(dc.clearColor.red, dc.clearColor.green, dc.clearColor.blue, dc.clearColor.alpha); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); +}; + +// Internal function. Intentionally not documented. +WorldWindow.prototype.doDraw = function () { + this.drawContext.renderShapes = true; + + if (this.subsurfaceMode && this.hasStencilBuffer) { + // Draw the surface and collect the ordered renderables. + this.drawContext.currentGlContext.disable(this.drawContext.currentGlContext.STENCIL_TEST); + this.drawContext.surfaceShapeTileBuilder.clear(); + this.drawLayers(true); + this.drawSurfaceRenderables(); + this.drawContext.surfaceShapeTileBuilder.doRender(this.drawContext); + + if (!this.deferOrderedRendering) { + // Clear the depth and stencil buffers prior to rendering the ordered renderables. This allows + // sub-surface renderables to be drawn beneath the terrain. Turn on stenciling to capture the + // fragments that ordered renderables draw. The terrain and surface shapes will be subsequently + // drawn again, and the stencil buffer will ensure that they are drawn only where they overlap + // the fragments drawn by the ordered renderables. + this.drawContext.currentGlContext.clear( + this.drawContext.currentGlContext.DEPTH_BUFFER_BIT | this.drawContext.currentGlContext.STENCIL_BUFFER_BIT); + this.drawContext.currentGlContext.enable(this.drawContext.currentGlContext.STENCIL_TEST); + this.drawContext.currentGlContext.stencilFunc(this.drawContext.currentGlContext.ALWAYS, 1, 1); + this.drawContext.currentGlContext.stencilOp( + this.drawContext.currentGlContext.REPLACE, this.drawContext.currentGlContext.REPLACE, this.drawContext.currentGlContext.REPLACE); + this.drawOrderedRenderables(); + } + } else { + this.drawContext.surfaceShapeTileBuilder.clear(); + this.drawLayers(true); + this.drawSurfaceRenderables(); + this.drawContext.surfaceShapeTileBuilder.doRender(this.drawContext); + + if (!this.deferOrderedRendering) { + this.drawOrderedRenderables(); + this.drawScreenRenderables(); + } + } +}; + +WorldWindow.prototype.redrawSurface = function () { + // Draw the terrain and surface shapes but only where the current stencil buffer is non-zero. + // The non-zero fragments are from drawing the ordered renderables previously. + this.drawContext.currentGlContext.enable(this.drawContext.currentGlContext.STENCIL_TEST); + this.drawContext.currentGlContext.stencilFunc(this.drawContext.currentGlContext.EQUAL, 1, 1); + this.drawContext.currentGlContext.stencilOp( + this.drawContext.currentGlContext.KEEP, this.drawContext.currentGlContext.KEEP, this.drawContext.currentGlContext.KEEP); + this.drawContext.surfaceShapeTileBuilder.clear(); + this.drawLayers(false); + this.drawSurfaceRenderables(); + this.drawContext.surfaceShapeTileBuilder.doRender(this.drawContext); + this.drawContext.currentGlContext.disable(this.drawContext.currentGlContext.STENCIL_TEST); +}; + +// Internal function. Intentionally not documented. +WorldWindow.prototype.doPick = function () { + if (this.drawContext.terrain) { + this.drawContext.terrain.pick(this.drawContext); + } + + if (!this.drawContext.pickTerrainOnly) { + if (this.subsurfaceMode && this.hasStencilBuffer) { + // Draw the surface and collect the ordered renderables. + this.drawContext.currentGlContext.disable(this.drawContext.currentGlContext.STENCIL_TEST); + this.drawContext.surfaceShapeTileBuilder.clear(); + this.drawLayers(true); + this.drawSurfaceRenderables(); + this.drawContext.surfaceShapeTileBuilder.doRender(this.drawContext); + + if (!this.deferOrderedRendering) { + // Clear the depth and stencil buffers prior to rendering the ordered renderables. This allows + // sub-surface renderables to be drawn beneath the terrain. Turn on stenciling to capture the + // fragments that ordered renderables draw. The terrain and surface shapes will be subsequently + // drawn again, and the stencil buffer will ensure that they are drawn only where they overlap + // the fragments drawn by the ordered renderables. + this.drawContext.currentGlContext.clear( + this.drawContext.currentGlContext.DEPTH_BUFFER_BIT | this.drawContext.currentGlContext.STENCIL_BUFFER_BIT); + this.drawContext.currentGlContext.enable(this.drawContext.currentGlContext.STENCIL_TEST); + this.drawContext.currentGlContext.stencilFunc(this.drawContext.currentGlContext.ALWAYS, 1, 1); + this.drawContext.currentGlContext.stencilOp( + this.drawContext.currentGlContext.REPLACE, this.drawContext.currentGlContext.REPLACE, this.drawContext.currentGlContext.REPLACE); + this.drawOrderedRenderables(); + this.drawContext.terrain.pick(this.drawContext); + this.drawScreenRenderables(); + } + } else { + this.drawContext.surfaceShapeTileBuilder.clear(); + + this.drawLayers(true); + this.drawSurfaceRenderables(); + + this.drawContext.surfaceShapeTileBuilder.doRender(this.drawContext); + + if (!this.deferOrderedRendering) { + this.drawOrderedRenderables(); + this.drawScreenRenderables(); + } + } + } +}; + +// Internal function. Intentionally not documented. +WorldWindow.prototype.createTerrain = function () { + var dc = this.drawContext; + dc.terrain = this.globe.tessellator.tessellate(dc); + dc.frameStatistics.setTerrainTileCount(dc.terrain ? dc.terrain.surfaceGeometry.length : 0); +}; + +WorldWindow.prototype.makeCurrent = function (offset) { + var dc = this.drawContext; + dc.globe.offset = offset; + dc.globeStateKey = dc.globe.stateKey; + + switch (offset) { + case -1: + dc.terrain = this.terrainLeft; + break; + + case 0: + dc.terrain = this.terrainCenter; + break; + + case 1: + dc.terrain = this.terrainRight; + break; + } +}; + +WorldWindow.prototype.createTerrain2DContiguous = function () { + var dc = this.drawContext; + + this.terrainCenter = null; + dc.globe.offset = 0; + dc.globeStateKey = dc.globe.stateKey; + if (dc.globe.intersectsFrustum(dc.frustumInModelCoordinates)) { + this.terrainCenter = dc.globe.tessellator.tessellate(dc); + } + + this.terrainRight = null; + dc.globe.offset = 1; + dc.globeStateKey = dc.globe.stateKey; + if (dc.globe.intersectsFrustum(dc.frustumInModelCoordinates)) { + this.terrainRight = dc.globe.tessellator.tessellate(dc); + } + + this.terrainLeft = null; + dc.globe.offset = -1; + dc.globeStateKey = dc.globe.stateKey; + if (dc.globe.intersectsFrustum(dc.frustumInModelCoordinates)) { + this.terrainLeft = dc.globe.tessellator.tessellate(dc); + } +}; + +WorldWindow.prototype.draw2DContiguous = function () { + var drawing = ""; + + if (this.terrainCenter) { + drawing += " 0 "; + this.makeCurrent(0); + this.deferOrderedRendering = this.terrainLeft || this.terrainRight; + this.doDraw(); + } + + if (this.terrainRight) { + drawing += " 1 "; + this.makeCurrent(1); + this.deferOrderedRendering = this.terrainLeft || this.terrainLeft; + this.doDraw(); + } + + this.deferOrderedRendering = false; + + if (this.terrainLeft) { + drawing += " -1 "; + this.makeCurrent(-1); + this.doDraw(); + } + // + //console.log(drawing); + + if (this.subsurfaceMode && this.hasStencilBuffer) { + this.deferOrderedRendering = true; + + if (this.terrainCenter) { + drawing += " 0 "; + this.makeCurrent(0); + this.redrawSurface(); + } + + if (this.terrainRight) { + drawing += " 1 "; + this.makeCurrent(1); + this.redrawSurface(); + } + + if (this.terrainLeft) { + drawing += " -1 "; + this.makeCurrent(-1); + this.redrawSurface(); + } + } + + this.drawScreenRenderables(); +}; + +WorldWindow.prototype.pick2DContiguous = function () { + if (this.terrainCenter) { + this.makeCurrent(0); + this.deferOrderedRendering = this.terrainLeft || this.terrainRight; + this.doPick(); + } + + if (this.terrainRight) { + this.makeCurrent(1); + this.deferOrderedRendering = this.terrainLeft || this.terrainLeft; + this.doPick(); + } + + this.deferOrderedRendering = false; + + if (this.terrainLeft) { + this.makeCurrent(-1); + this.doPick(); + } +}; + +// Internal function. Intentionally not documented. +WorldWindow.prototype.drawLayers = function (accumulateOrderedRenderables) { + // Draw all the layers attached to this WorldWindow. + + var beginTime = Date.now(), + dc = this.drawContext, + layers = dc.layers, + layer; + + dc.accumulateOrderedRenderables = accumulateOrderedRenderables; + + for (var i = 0, len = layers.length; i < len; i++) { + layer = layers[i]; + if (layer) { + dc.currentLayer = layer; + try { + layer.render(dc); + } catch (e) { + Logger.log(Logger.LEVEL_SEVERE, "Error while rendering layer " + layer.displayName + ".\n" + + e.toString()); + // Keep going. Render the rest of the layers. + } + } + } + dc.currentLayer = null; + var now = Date.now(); + dc.frameStatistics.layerRenderingTime = now - beginTime; +}; + +/** + * Adds a specified layer to the end of this WorldWindow. + * @param {Layer} layer The layer to add. May be null or undefined, in which case this WorldWindow is not + * modified. + */ +WorldWindow.prototype.addLayer = function (layer) { + if (layer) { + this.layers.push(layer); + } +}; + +/** + * Removes the first instance of a specified layer from this WorldWindow. + * @param {Layer} layer The layer to remove. May be null or undefined, in which case this WorldWindow is not + * modified. This WorldWindow is also not modified if the specified layer does not exist in this WorldWindow's + * layer list. + */ +WorldWindow.prototype.removeLayer = function (layer) { + var index = this.indexOfLayer(layer); + if (index >= 0) { + this.layers.splice(index, 1); + } +}; + +/** + * Inserts a specified layer at a specified position in this WorldWindow. + * @param {Number} index The index at which to insert the layer. May be negative to specify the position + * from the end of the array. + * @param {Layer} layer The layer to insert. May be null or undefined, in which case this WorldWindow is not + * modified. + */ +WorldWindow.prototype.insertLayer = function (index, layer) { + if (layer) { + this.layers.splice(index, 0, layer); + } +}; + +/** + * Returns the index of a specified layer in this WorldWindow. + * @param {Layer} layer The layer to search for. + * @returns {Number} The index of the specified layer or -1 if it doesn't exist in this WorldWindow. + */ +WorldWindow.prototype.indexOfLayer = function (layer) { + return this.layers.indexOf(layer); +}; + +// Internal function. Intentionally not documented. +WorldWindow.prototype.drawSurfaceRenderables = function () { + var dc = this.drawContext, + sr; + + dc.reverseSurfaceRenderables(); + + while (sr = dc.popSurfaceRenderable()) { + try { + sr.renderSurface(dc); + } catch (e) { + Logger.logMessage(Logger.LEVEL_WARNING, "WorldWindow", "drawSurfaceRenderables", + "Error while rendering a surface renderable.\n" + e.message); + // Keep going. Render the rest of the surface renderables. + } + } +}; + +// Internal function. Intentionally not documented. +WorldWindow.prototype.drawOrderedRenderables = function () { + var beginTime = Date.now(), + dc = this.drawContext, + or; + + dc.sortOrderedRenderables(); + + if (this._orderedRenderingFilters) { + for (var f = 0; f < this._orderedRenderingFilters.length; f++) { + this._orderedRenderingFilters[f](this.drawContext); + } + } + + dc.orderedRenderingMode = true; + + while (or = dc.popOrderedRenderable()) { + try { + or.renderOrdered(dc); + } catch (e) { + Logger.logMessage(Logger.LEVEL_WARNING, "WorldWindow", "drawOrderedRenderables", + "Error while rendering an ordered renderable.\n" + e.message); + // Keep going. Render the rest of the ordered renderables. + } + } + + dc.orderedRenderingMode = false; + dc.frameStatistics.orderedRenderingTime = Date.now() - beginTime; +}; + +WorldWindow.prototype.drawScreenRenderables = function () { + var dc = this.drawContext, + or; + + while (or = dc.nextScreenRenderable()) { + try { + or.renderOrdered(dc); + } catch (e) { + Logger.logMessage(Logger.LEVEL_WARNING, "WorldWindow", "drawOrderedRenderables", + "Error while rendering a screen renderable.\n" + e.message); + // Keep going. Render the rest of the screen renderables. + } + } +}; + +// Internal function. Intentionally not documented. +WorldWindow.prototype.resolveTopPick = function () { + if (this.drawContext.objectsAtPickPoint.objects.length == 0) { + return; // nothing picked; avoid calling readPickColor unnecessarily + } + + // Make a last reading to determine what's on top. + + var pickedObjects = this.drawContext.objectsAtPickPoint, + pickColor = this.drawContext.readPickColor(this.drawContext.pickPoint), + topObject = null, + terrainObject = null; + + if (pickColor) { + // Find the picked object with the top color code and set its isOnTop flag. + for (var i = 0, len = pickedObjects.objects.length; i < len; i++) { + var po = pickedObjects.objects[i]; + + if (po.isTerrain) { + terrainObject = po; + } + + if (po.color.equals(pickColor)) { + po.isOnTop = true; + topObject = po; + + if (terrainObject) { + break; // no need to search for more than the top object and the terrain object + } + } + } + + // In single-pick mode provide only the top-most object and the terrain object, if any. + if (!this.drawContext.deepPicking) { + pickedObjects.clear(); + if (topObject) { + pickedObjects.add(topObject); + } + if (terrainObject && terrainObject != topObject) { + pickedObjects.add(terrainObject); + } + } + } else { + pickedObjects.clear(); // nothing drawn at the pick point + } +}; + +// Internal. Intentionally not documented. +WorldWindow.prototype.resolveTerrainPick = function () { + var pickedObjects = this.drawContext.objectsAtPickPoint, + po; + + // Mark the first picked terrain object as "on top". The picked object list should contain only one entry + // indicating the picked terrain object, but we iterate over the list contents anyway. + for (var i = 0, len = pickedObjects.objects.length; i < len; i++) { + po = pickedObjects.objects[i]; + if (po.isTerrain) { + po.isOnTop = true; + break; + } + } +}; + +// Internal. Intentionally not documented. +WorldWindow.prototype.resolveRegionPick = function () { + if (this.drawContext.objectsAtPickPoint.objects.length == 0) { + return; // nothing picked; avoid calling readPickColors unnecessarily + } + + // Mark every picked object with a color in the pick buffer as "on top". + + var pickedObjects = this.drawContext.objectsAtPickPoint, + uniquePickColors = this.drawContext.readPickColors(this.drawContext.pickRectangle), + po, + color; + + for (var i = 0, len = pickedObjects.objects.length; i < len; i++) { + po = pickedObjects.objects[i]; + if (!po) continue; + var poColor = po.color.toByteString(); + color = uniquePickColors[poColor]; + if (color) { + po.isOnTop = true; + } else if (po.userObject instanceof SurfaceShape) { + // SurfaceShapes ALWAYS get added to the pick list, since their rendering is deferred + // until the tile they are cached by is rendered. So a SurfaceShape may be in the pick list + // but is not seen in the pick rectangle. + // + // Remove the SurfaceShape that was not visible to the pick rectangle. + pickedObjects.objects.splice(i, 1); + i -= 1; + } + } +}; + +// Internal. Intentionally not documented. +WorldWindow.prototype.callRedrawCallbacks = function (stage) { + for (var i = 0, len = this._redrawCallbacks.length; i < len; i++) { + try { + this._redrawCallbacks[i](this, stage); + } catch (e) { + Logger.log(Logger.LEVEL_SEVERE, "Exception calling redraw callback.\n" + e.toString()); + // Keep going. Execute the rest of the callbacks. + } + } +}; + +/** + * Moves this WorldWindow's navigator to a specified location or position. + * @param {Location | Position} position The location or position to move the navigator to. If this + * argument contains an "altitude" property, as {@link Position} does, the end point of the navigation is + * at the specified altitude. Otherwise the end point is at the current altitude of the navigator. + * + * This function uses this WorldWindow's {@link GoToAnimator} property to perform the move. That object's + * properties can be specified by the application to modify its behavior during calls to this function. + * It's cancel method can also be used to cancel the move initiated by this function. + * @param {Function} completionCallback If not null or undefined, specifies a function to call when the + * animation completes. The completion callback is called with a single argument, this animator. + * @throws {ArgumentError} If the specified location or position is null or undefined. + */ +WorldWindow.prototype.goTo = function (position, completionCallback) { + this.goToAnimator.goTo(position, completionCallback); +}; + +/** + * Declutters the current ordered renderables with a specified group ID. This function is not called by + * applications directly. It's meant to be invoked as an ordered rendering filter in this WorldWindow's + * [orderedRenderingFilters]{@link WorldWindow#orderedRenderingFilters} property. + *
+ * The function operates by setting the target visibility of occluded shapes to 0 and unoccluded shapes to 1. + * @param {DrawContext} dc The current draw context. + * @param {Number} groupId The ID of the group to declutter. Must not be null, undefined or 0. + * @throws {ArgumentError} If the specified group ID is null, undefined or 0. + */ +WorldWindow.prototype.declutter = function (dc, groupId) { + if (!groupId) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "declutter", + "Group ID is null, undefined or 0.")); + } + + // Collect all the declutterables in the specified group. + var declutterables = []; + for (var i = 0; i < dc.orderedRenderables.length; i++) { + var orderedRenderable = dc.orderedRenderables[i].orderedRenderable; + if (orderedRenderable.declutterGroup === groupId) { + declutterables.push(orderedRenderable); + } + } + + // Filter the declutterables by determining which are partially occluded. Since the ordered renderable + // list was already sorted from front to back, the front-most will represent an entire occluded group. + var rects = []; + for (var j = 0; j < declutterables.length; j++) { + var declutterable = declutterables[j], + screenBounds = declutterable.screenBounds; + + if (screenBounds && screenBounds.intersectsRectangles(rects)) { + declutterable.targetVisibility = 0; + } else { + declutterable.targetVisibility = 1; + if (screenBounds) { + rects.push(screenBounds); + } + } + } +}; + +/** + * Computes a ray originating at the eyePoint and extending through the specified point in window + * coordinates. + *
+ * The specified point is understood to be in the window coordinate system of the WorldWindow, with the origin + * in the top-left corner and axes that extend down and to the right from the origin point. + *
+ * The results of this method are undefined if the specified point is outside of the WorldWindow's + * bounds. + * + * @param {Vec2} point The window coordinates point to compute a ray for. + * @returns {Line} A new Line initialized to the origin and direction of the computed ray, or null if the + * ray could not be computed. + */ +WorldWindow.prototype.rayThroughScreenPoint = function (point) { + if (!point) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "rayThroughScreenPoint", + "missingPoint")); + } + + // Convert the point's xy coordinates from window coordinates to WebGL screen coordinates. + var screenPoint = new Vec3(point[0], this.viewport.height - point[1], 0), + nearPoint = new Vec3(0, 0, 0), + farPoint = new Vec3(0, 0, 0); + + this.computeViewingTransform(this.scratchProjection, this.scratchModelview); + var modelviewProjection = Matrix.fromIdentity(); + modelviewProjection.setToMultiply(this.scratchProjection, this.scratchModelview); + var modelviewProjectionInv = Matrix.fromIdentity(); + modelviewProjectionInv.invertMatrix(modelviewProjection); + + // Compute the model coordinate point on the near clip plane with the xy coordinates and depth 0. + if (!modelviewProjectionInv.unProject(screenPoint, this.viewport, nearPoint)) { + return null; + } + + // Compute the model coordinate point on the far clip plane with the xy coordinates and depth 1. + screenPoint[2] = 1; + if (!modelviewProjectionInv.unProject(screenPoint, this.viewport, farPoint)) { + return null; + } + + var eyePoint = this.scratchModelview.extractEyePoint(new Vec3(0, 0, 0)); + + // Compute a ray originating at the eye point and with direction pointing from the xy coordinate on the near + // plane to the same xy coordinate on the far plane. + var origin = new Vec3(eyePoint[0], eyePoint[1], eyePoint[2]), + direction = new Vec3(farPoint[0], farPoint[1], farPoint[2]); + + direction.subtract(nearPoint); + direction.normalize(); + + return new Line(origin, direction); +}; + +export default WorldWindow; diff --git a/web/test/WebWorldWind/src/WorldWindowController.js b/web/test/WebWorldWind/src/WorldWindowController.js new file mode 100644 index 00000000..f49b2bd7 --- /dev/null +++ b/web/test/WebWorldWind/src/WorldWindowController.js @@ -0,0 +1,120 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports WorldWindowController + */ +import ArgumentError from './error/ArgumentError'; +import Logger from './util/Logger'; +import UnsupportedOperationError from './error/UnsupportedOperationError'; + + +/** + * Constructs a root window controller. + * @alias WorldWindowController + * @constructor + * @abstract + * @classDesc This class provides a base window controller with required properties and methods which sub-classes may + * inherit from to create custom window controllers for controlling the globe via user interaction. + * @param {WorldWindow} worldWindow The WorldWindow associated with this layer. + * @throws {ArgumentError} If the specified WorldWindow is null or undefined. + */ +function WorldWindowController(worldWindow) { + if (!worldWindow) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindowController", "constructor", "missingWorldWindow")); + } + + /** + * The WorldWindow associated with this controller. + * @type {WorldWindow} + * @readonly + */ + this.wwd = worldWindow; + + // Intentionally not documented. + this.allGestureListeners = []; +} + +// Intentionally not documented. +WorldWindowController.prototype.onGestureEvent = function (event) { + var handled = false; + + for (var i = 0; i < this.allGestureListeners.length && !handled; i++) { + handled |= this.allGestureListeners[i].onGestureEvent(event); + } + + return handled; +}; + +// Intentionally not documented. +WorldWindowController.prototype.gestureStateChanged = function (recognizer) { + throw new UnsupportedOperationError( + Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindowController", "gestureStateChanged", "abstractInvocation")); +}; + +/** + * Registers a gesture event listener on this controller. Registering event listeners using this function + * enables applications to prevent the controller's default behavior. + * + * Listeners must implement an onGestureEvent method to receive event notifications. The onGestureEvent method will + * receive one parameter containing the information about the gesture event. Returning true from onGestureEvent + * indicates that the event was processed and will prevent any further handling of the event. + * + * When an event occurs, application event listeners are called before WorldWindowController event listeners. + * + * @param listener The function to call when the event occurs. + * @throws {ArgumentError} If any argument is null or undefined. + */ +WorldWindowController.prototype.addGestureListener = function (listener) { + if (!listener) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindowController", "addGestureListener", "missingListener")); + } + + this.allGestureListeners.push(listener); +}; + +/** + * Removes a gesture event listener from this controller. The listener must be the same object passed to + * addGestureListener. Calling removeGestureListener with arguments that do not identify a currently registered + * listener has no effect. + * + * @param listener The listener to remove. Must be the same object passed to addGestureListener. + * @throws {ArgumentError} If any argument is null or undefined. + */ +WorldWindowController.prototype.removeGestureListener = function (listener) { + if (!listener) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindowController", "removeGestureListener", "missingListener")); + } + + var index = this.allGestureListeners.indexOf(listener); + if (index !== -1) { + this.allGestureListeners.splice(index, 1); // remove the listener from the list + } +}; + +/** + * Called by WorldWindow to allow the controller to enforce navigation limits. Implementation is not required by + * sub-classes. + */ +WorldWindowController.prototype.applyLimits = function () { + +}; + +export default WorldWindowController; + diff --git a/web/test/WebWorldWind/src/cache/GpuResourceCache.js b/web/test/WebWorldWind/src/cache/GpuResourceCache.js new file mode 100644 index 00000000..e664e4cf --- /dev/null +++ b/web/test/WebWorldWind/src/cache/GpuResourceCache.js @@ -0,0 +1,272 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports GpuResourceCache + */ +import AbsentResourceList from '../util/AbsentResourceList'; +import ArgumentError from '../error/ArgumentError'; +import ImageSource from '../util/ImageSource'; +import Logger from '../util/Logger'; +import MemoryCache from '../cache/MemoryCache'; +import Texture from '../render/Texture'; + + +/** + * Constructs a GPU resource cache for a specified size and low-water value. + * @alias GpuResourceCache + * @constructor + * @classdesc Maintains a cache of GPU resources such as textures and GLSL programs. + * Applications typically do not interact with this class unless they create their own shapes. + * @param {Number} capacity The cache capacity, in bytes. + * @param {Number} lowWater The number of bytes to clear the cache to when it exceeds its capacity. + * @throws {ArgumentError} If the specified capacity is undefined, 0 or negative or the low-water value is + * undefined, negative or not less than the capacity. + */ +function GpuResourceCache(capacity, lowWater) { + if (!capacity || capacity < 1) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "GpuResourceCache", "constructor", + "Specified cache capacity is undefined, 0 or negative.")); + } + + if (!lowWater || lowWater < 0 || lowWater >= capacity) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "GpuResourceCache", "constructor", + "Specified cache low-water value is undefined, negative or not less than the capacity.")); + } + + // Private. Holds the actual cache entries. + this.entries = new MemoryCache(capacity, lowWater); + + // Private. Counter for generating cache keys. + this.cacheKeyPool = 0; + + // Private. List of retrievals currently in progress. + this.currentRetrievals = {}; + + // Private. Identifies requested resources that whose retrieval failed. + this.absentResourceList = new AbsentResourceList(3, 60e3); +} + +Object.defineProperties(GpuResourceCache.prototype, { + /** + * Indicates the capacity of this cache in bytes. + * @type {Number} + * @readonly + * @memberof GpuResourceCache.prototype + */ + capacity: { + get: function () { + return this.entries.capacity; + } + }, + + /** + * Indicates the low-water value for this cache in bytes, the size this cache is cleared to when it + * exceeds its capacity. + * @type {Number} + * @readonly + * @memberof GpuResourceCache.prototype + */ + lowWater: { + get: function () { + return this.entries.lowWater; + } + }, + + /** + * Indicates the number of bytes currently used by this cache. + * @type {Number} + * @readonly + * @memberof GpuResourceCache.prototype + */ + usedCapacity: { + get: function () { + return this.entries.usedCapacity; + } + }, + + /** + * Indicates the number of free bytes in this cache. + * @type {Number} + * @readonly + * @memberof GpuResourceCache.prototype + */ + freeCapacity: { + get: function () { + return this.entries.freeCapacity; + } + } +}); + +/** + * Creates a cache key unique to this cache, typically for a resource about to be added to this cache. + * @returns {String} The generated cache key. + */ +GpuResourceCache.prototype.generateCacheKey = function () { + return "GpuResourceCache " + ++this.cacheKeyPool; +}; + +/** + * Adds a specified resource to this cache. Replaces the existing resource for the specified key if the + * cache currently contains a resource for that key. + * @param {String|ImageSource} key The key or image source of the resource to add. + * @param {Object} resource The resource to add to the cache. + * @param {Number} size The resource's size in bytes. Must be greater than 0. + * @throws {ArgumentError} If either the key or resource arguments is null or undefined + * or if the specified size is less than 1. + */ +GpuResourceCache.prototype.putResource = function (key, resource, size) { + if (!key) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "GpuResourceCache", "putResource", "missingKey.")); + } + + if (!resource) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "GpuResourceCache", "putResource", "missingResource.")); + } + + if (!size || size < 1) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "GpuResourceCache", "putResource", + "The specified resource size is undefined or less than 1.")); + } + + var entry = { + resource: resource + }; + + this.entries.putEntry(key instanceof ImageSource ? key.key : key, entry, size); +}; + +/** + * Returns the resource associated with a specified key. + * @param {String|ImageSource} key The key or image source of the resource to find. + * @returns {Object} The resource associated with the specified key, or null if the resource is not in + * this cache or the specified key is null or undefined. + */ +GpuResourceCache.prototype.resourceForKey = function (key) { + var entry = key instanceof ImageSource + ? this.entries.entryForKey(key.key) : this.entries.entryForKey(key); + + var resource = entry ? entry.resource : null; + + // This is faster than checking if the resource is a texture using instanceof. + if (resource !== null && typeof resource.clearTexParameters === "function") { + resource.clearTexParameters(); + } + + return resource; +}; + +/** + * Sets a resource's aging factor (multiplier). + * @param {String} key The key of the resource to modify. If null or undefined, the resource's cache entry is not modified. + * @param {Number} agingFactor A multiplier applied to the age of the resource. + */ +GpuResourceCache.prototype.setResourceAgingFactor = function (key, agingFactor) { + this.entries.setEntryAgingFactor(key, agingFactor); +}; + +/** + * Indicates whether a specified resource is in this cache. + * @param {String|ImageSource} key The key or image source of the resource to find. + * @returns {Boolean} true If the resource is in this cache, false if the resource + * is not in this cache or the specified key is null or undefined. + */ +GpuResourceCache.prototype.containsResource = function (key) { + return this.entries.containsKey(key instanceof ImageSource ? key.key : key); +}; + +/** + * Removes the specified resource from this cache. The cache is not modified if the specified key is null or + * undefined or does not correspond to an entry in the cache. + * @param {String|ImageSource} key The key or image source of the resource to remove. + */ +GpuResourceCache.prototype.removeResource = function (key) { + this.entries.removeEntry(key instanceof ImageSource ? key.key : key); +}; + +/** + * Removes all resources from this cache. + */ +GpuResourceCache.prototype.clear = function () { + this.entries.clear(false); +}; + +/** + * Retrieves an image and adds it to this cache when it arrives. If the specified image source is a URL, a + * retrieval request for the image is made and this method returns immediately with a value of null. A redraw + * event is generated when the image subsequently arrives and is added to this cache. If the image source is an + * {@link ImageSource}, the image is used immediately and this method returns the {@link Texture} created and + * cached for the image. No redraw event is generated in this case. + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {String|ImageSource} imageSource The image source, either a {@link ImageSource} or a String + * giving the URL of the image. + * @param {GLenum} wrapMode Optional. Specifies the wrap mode of the texture. Defaults to gl.CLAMP_TO_EDGE + * @returns {Texture} The {@link Texture} created for the image if the specified image source is an + * {@link ImageSource}, otherwise null. + */ +GpuResourceCache.prototype.retrieveTexture = function (gl, imageSource, wrapMode) { + if (!imageSource) { + return null; + } + + if (imageSource instanceof ImageSource) { + var t = new Texture(gl, imageSource.image, wrapMode); + this.putResource(imageSource.key, t, t.size); + return t; + } + + if (this.currentRetrievals[imageSource] || this.absentResourceList.isResourceAbsent(imageSource)) { + return null; + } + + var cache = this, + image = new Image(); + + image.onload = function () { + Logger.log(Logger.LEVEL_INFO, "Image retrieval succeeded: " + imageSource); + + var texture = new Texture(gl, image, wrapMode); + + cache.putResource(imageSource, texture, texture.size); + + delete cache.currentRetrievals[imageSource]; + cache.absentResourceList.unmarkResourceAbsent(imageSource); + + // Send an event to request a redraw. + var e = document.createEvent('Event'); + e.initEvent(WorldWind.REDRAW_EVENT_TYPE, true, true); + window.dispatchEvent(e); + }; + + image.onerror = function () { + delete cache.currentRetrievals[imageSource]; + cache.absentResourceList.markResourceAbsent(imageSource); + Logger.log(Logger.LEVEL_WARNING, "Image retrieval failed: " + imageSource); + }; + + this.currentRetrievals[imageSource] = imageSource; + image.crossOrigin = 'anonymous'; + image.src = imageSource; + + return null; +}; + +export default GpuResourceCache; diff --git a/web/test/WebWorldWind/src/cache/MemoryCache.js b/web/test/WebWorldWind/src/cache/MemoryCache.js new file mode 100644 index 00000000..b9fff367 --- /dev/null +++ b/web/test/WebWorldWind/src/cache/MemoryCache.js @@ -0,0 +1,347 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports MemoryCache + */ +import ArgumentError from '../error/ArgumentError'; +import Logger from '../util/Logger'; + + +/** + * Constructs a memory cache of a specified size. + * @alias MemoryCache + * @constructor + * @classdesc Provides a limited-size memory cache of key-value pairs. The meaning of size depends on usage. + * Some instances of this class work in bytes while others work in counts. See the documentation for the + * specific use to determine the size units. + * @param {Number} capacity The cache's capacity. + * @param {Number} lowWater The size to clear the cache to when its capacity is exceeded. + * @throws {ArgumentError} If either the capacity is 0 or negative or the low-water value is greater than + * or equal to the capacity or less than 1. + */ +function MemoryCache(capacity, lowWater) { + if (!capacity || capacity < 1) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "MemoryCache", "constructor", + "The specified capacity is undefined, zero or negative")); + } + + if (!lowWater || lowWater >= capacity || lowWater < 0) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "MemoryCache", "constructor", + "The specified low-water value is undefined, greater than or equal to the capacity, or less than 1")); + } + + // Documented with its property accessor below. + this._capacity = capacity; + + // Documented with its property accessor below. + this._lowWater = lowWater; + + /** + * The size currently used by this cache. + * @type {Number} + * @readonly + */ + this.usedCapacity = 0; + + /** + * The size currently unused by this cache. + * @type {Number} + * @readonly + */ + this.freeCapacity = capacity; + + // Private. The cache entries. + this.entries = {}; + + // Private. The cache listeners. + this.listeners = []; +} + +Object.defineProperties(MemoryCache.prototype, { + /** + * The maximum this cache may hold. When the capacity is explicitly set via this property, and the current + * low-water value is greater than the specified capacity, the low-water value is adjusted to be 85% of + * the specified capacity. The specified capacity may not be less than or equal to 0. + * @type {Number} + * @memberof MemoryCache.prototype + */ + capacity: { + get: function () { + return this._capacity; + }, + set: function (value) { + if (!value || value < 1) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "MemoryCache", "capacity", + "Specified cache capacity is undefined, 0 or negative.")); + } + + var oldCapacity = this._capacity; + + this._capacity = value; + + if (this._capacity <= this.lowWater) { + this._lowWater = 0.85 * this._capacity; + } + + // Trim the cache to the low-water mark if it's less than the old capacity + if (this._capacity < oldCapacity) { + this.makeSpace(0); + } + } + }, + + /** + * The size to clear this cache to when its capacity is exceeded. It must be less than the current + * capacity and not negative. + * @type {Number} + * @memberof MemoryCache.prototype + */ + lowWater: { + get: function () { + return this._lowWater; + }, + set: function (value) { + if (!value || value >= this._capacity || value < 0) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "MemoryCache", "lowWater", + "Specified cache low-water value is undefined, negative or not less than the current capacity.")); + } + + this._lowWater = value; + } + } +}); + +/** + * Returns the entry for a specified key. + * @param {String} key The key of the entry to return. + * @returns {Object} The entry associated with the specified key, or null if the key is not in the cache or + * is null or undefined. + */ +MemoryCache.prototype.entryForKey = function (key) { + if (!key) + return null; + + var cacheEntry = this.entries[key]; + if (!cacheEntry) + return null; + + cacheEntry.lastUsed = Date.now(); + + return cacheEntry.entry; +}; + +/** + * Adds a specified entry to this cache. + * @param {String} key The entry's key. + * @param {Object} entry The entry. + * @param {Number} size The entry's size. + * @throws {ArgumentError} If the specified key or entry is null or undefined or the specified size is less + * than 1. + */ +MemoryCache.prototype.putEntry = function (key, entry, size) { + if (!key) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "MemoryCache", "putEntry", "missingKey.")); + } + + if (!entry) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "MemoryCache", "putEntry", "missingEntry.")); + } + + if (size < 1) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "MemoryCache", "putEntry", + "The specified entry size is less than 1.")); + } + + var existing = this.entries[key], + cacheEntry; + + if (existing) { + this.removeEntry(key); + } + + if (this.usedCapacity + size > this._capacity) { + this.makeSpace(size); + } + + this.usedCapacity += size; + this.freeCapacity = this._capacity - this.usedCapacity; + + cacheEntry = { + key: key, + entry: entry, + size: size, + lastUsed: Date.now(), + agingFactor: 1 // 1x = normal aging + }; + + this.entries[key] = cacheEntry; +}; + +/** + * Removes all resources from this cache. + * @param {Boolean} callListeners If true, the current cache listeners are called for each entry removed. + * If false, the cache listeners are not called. + */ +MemoryCache.prototype.clear = function (callListeners) { + if (callListeners) { + // Remove each entry individually so that the listeners can be called for each entry. + for (var key in this.entries) { + if (this.entries.hasOwnProperty(key)) { + this.removeCacheEntry(key); + } + } + } + + this.entries = {}; + this.freeCapacity = this._capacity; + this.usedCapacity = 0; +}; + +/** + * Remove an entry from this cache. + * @param {String} key The key of the entry to remove. If null or undefined, this cache is not modified. + */ +MemoryCache.prototype.removeEntry = function (key) { + if (!key) + return; + + var cacheEntry = this.entries[key]; + if (cacheEntry) { + this.removeCacheEntry(cacheEntry); + } +}; + +/** + * Sets an entry's aging factor (multiplier) used to sort the entries for eviction. + * A value of one is normal aging; a value of two invokes 2x aging, causing + * the entry to become twice as old as a normal sibling with the same + * 'last used' timestamp. Setting a value of zero would be a "fountain + * of youth" for an entry as it wouldn't age and thus would sort to the + * bottom of the eviction queue. + * @param {String} key The key of the entry to modify. If null or undefined, the cache entry is not modified. + * @param {Number} agingFactor A multiplier applied to the age of the entry when sorting candidates for eviction. + * + */ +MemoryCache.prototype.setEntryAgingFactor = function (key, agingFactor) { + if (!key) + return; + + var cacheEntry = this.entries[key]; + if (cacheEntry) { + cacheEntry.agingFactor = agingFactor; + } +}; + +// Private. Removes a specified entry from this cache. +MemoryCache.prototype.removeCacheEntry = function (cacheEntry) { + // All removal passes through this function. + + delete this.entries[cacheEntry.key]; + + this.usedCapacity -= cacheEntry.size; + this.freeCapacity = this._capacity - this.usedCapacity; + + for (var i = 0, len = this.listeners.length; i < len; i++) { + try { + this.listeners[i].entryRemoved(cacheEntry.key, cacheEntry.entry); + } catch (e) { + this.listeners[i].removalError(e, cacheEntry.key, cacheEntry.entry); + } + } +}; + +/** + * Indicates whether a specified entry is in this cache. + * @param {String} key The key of the entry to search for. + * @returns {Boolean} true if the entry exists, otherwise false. + */ +MemoryCache.prototype.containsKey = function (key) { + return key && this.entries[key]; +}; + +/** + * Adds a cache listener to this cache. + * @param {MemoryCacheListener} listener The listener to add. + * @throws {ArgumentError} If the specified listener is null or undefined or does not implement both the + * entryRemoved and removalError functions. + */ +MemoryCache.prototype.addCacheListener = function (listener) { + if (!listener) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "MemoryCache", "addCacheListener", "missingListener")); + } + + if (typeof listener.entryRemoved !== "function" || typeof listener.removalError !== "function") { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "MemoryCache", "addCacheListener", + "The specified listener does not implement the required functions.")); + } + + this.listeners.push(listener); +}; + +/** + * Removes a cache listener from this cache. + * @param {MemoryCacheListener} listener The listener to remove. + * @throws {ArgumentError} If the specified listener is null or undefined. + */ +MemoryCache.prototype.removeCacheListener = function (listener) { + if (!listener) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "MemoryCache", "removeCacheListener", "missingListener")); + } + + var index = this.listeners.indexOf(listener); + if (index > -1) { + this.listeners.splice(index, 1); + } +}; + +// Private. Clears this cache to that necessary to contain a specified amount of free space. +MemoryCache.prototype.makeSpace = function (spaceRequired) { + var sortedEntries = [], + now = Date.now(); + + // Sort the entries from least recently used to most recently used, then remove the least recently used entries + // until the cache capacity reaches the low water and the cache has enough free capacity for the required + // space. + for (var key in this.entries) { + if (this.entries.hasOwnProperty(key)) { + sortedEntries.push(this.entries[key]); + } + } + sortedEntries.sort(function (a, b) { + var aAge = (now - a.lastUsed) * a.agingFactor, + bAge = (now - b.lastUsed) * b.agingFactor; + return bAge - aAge; + }); + + for (var i = 0, len = sortedEntries.length; i < len; i++) { + if (this.usedCapacity > this._lowWater || this.freeCapacity < spaceRequired) { + this.removeCacheEntry(sortedEntries[i]); + } else { + break; + } + } +}; + +export default MemoryCache; diff --git a/web/test/WebWorldWind/src/cache/MemoryCacheListener.js b/web/test/WebWorldWind/src/cache/MemoryCacheListener.js new file mode 100644 index 00000000..8e1c7c1b --- /dev/null +++ b/web/test/WebWorldWind/src/cache/MemoryCacheListener.js @@ -0,0 +1,56 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Defines an interface for {@link MemoryCache} listeners. + * @exports MemoryCacheListener + * @interface MemoryCacheListener + */ +import Logger from '../util/Logger'; +import UnsupportedOperationError from '../error/UnsupportedOperationError'; + + +/** + * @alias MemoryCacheListener + * @constructor + */ +function MemoryCacheListener() { +} + +/** + * Called when an entry is removed from the cache. + * Implementers of this interface must implement this function. + * @param {String} key The key of the entry removed. + * @param {Object} entry The entry removed. + */ +MemoryCacheListener.prototype.entryRemoved = function (key, entry) { + throw new UnsupportedOperationError( + Logger.logMessage(Logger.LEVEL_SEVERE, "MemoryCacheListener", "entryRemoved", "abstractInvocation")); +}; + +/** + * Called when an error occurs during entry removal. + * Implementers of this interface must implement this function. + * @param {Object} error The error object describing the error that occurred. + * @param {String} key The key of the entry being removed. + * @param {Object} entry The entry being removed. + */ +MemoryCacheListener.prototype.removalError = function (error, key, entry) { + throw new UnsupportedOperationError( + Logger.logMessage(Logger.LEVEL_SEVERE, "MemoryCacheListener", "removalError", "abstractInvocation")); +}; + +export default MemoryCacheListener; diff --git a/web/test/WebWorldWind/src/error/AbstractError.js b/web/test/WebWorldWind/src/error/AbstractError.js new file mode 100644 index 00000000..6bdb028a --- /dev/null +++ b/web/test/WebWorldWind/src/error/AbstractError.js @@ -0,0 +1,53 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports AbstractError + */ + + + +/** + * Constructs an error with a specified name and message. + * @alias AbstractError + * @constructor + * @abstract + * @classdesc Provides an abstract base class for error classes. This class is not meant to be instantiated + * directly but used only by subclasses. + * @param {String} name The error's name. + * @param {String} message The message. + */ +function AbstractError(name, message) { + this.name = name; + this.message = message; +} + +/** + * Returns the message and stack trace associated with this error. + * @returns {String} The message and stack trace associated with this error. + */ +AbstractError.prototype.toString = function () { + var str = this.name + ': ' + this.message; + + if (this.stack) { + str += '\n' + this.stack.toString(); + } + + return str; +}; + +export default AbstractError; + diff --git a/web/test/WebWorldWind/src/error/ArgumentError.js b/web/test/WebWorldWind/src/error/ArgumentError.js new file mode 100644 index 00000000..3929b8f7 --- /dev/null +++ b/web/test/WebWorldWind/src/error/ArgumentError.js @@ -0,0 +1,46 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports ArgumentError + */ +import AbstractError from '../error/AbstractError'; + + +/** + * Constructs an argument error with a specified message. + * @alias ArgumentError + * @constructor + * @classdesc Represents an error associated with invalid function arguments. + * @augments AbstractError + * @param {String} message The message. + */ +function ArgumentError(message) { + AbstractError.call(this, "ArgumentError", message); + + var stack; + try { + //noinspection ExceptionCaughtLocallyJS + throw new Error(); + } catch (e) { + stack = e.stack; + } + this.stack = stack; +} + +ArgumentError.prototype = Object.create(AbstractError.prototype); + +export default ArgumentError; diff --git a/web/test/WebWorldWind/src/error/NotYetImplementedError.js b/web/test/WebWorldWind/src/error/NotYetImplementedError.js new file mode 100644 index 00000000..16e6b456 --- /dev/null +++ b/web/test/WebWorldWind/src/error/NotYetImplementedError.js @@ -0,0 +1,46 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports NotYetImplementedError + */ +import AbstractError from '../error/AbstractError'; + + +/** + * Constructs a not-yet-implemented error with a specified message. + * @alias NotYetImplementedError + * @constructor + * @classdesc Represents an error associated with an operation that is not yet implemented. + * @augments AbstractError + * @param {String} message The message. + */ +function NotYetImplementedError(message) { + AbstractError.call(this, "NotYetImplementedError", message); + + var stack; + try { + //noinspection ExceptionCaughtLocallyJS + throw new Error(); + } catch (e) { + stack = e.stack; + } + this.stack = stack; +} + +NotYetImplementedError.prototype = Object.create(AbstractError.prototype); + +export default NotYetImplementedError; diff --git a/web/test/WebWorldWind/src/error/UnsupportedOperationError.js b/web/test/WebWorldWind/src/error/UnsupportedOperationError.js new file mode 100644 index 00000000..1cd17d3a --- /dev/null +++ b/web/test/WebWorldWind/src/error/UnsupportedOperationError.js @@ -0,0 +1,48 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports UnsupportedOperationError + */ +import AbstractError from '../error/AbstractError'; + + +/** + * Constructs an unsupported-operation error with a specified message. + * @alias UnsupportedOperationError + * @constructor + * @classdesc Represents an error associated with an operation that is not available or should not be invoked. + * Typically raised when an abstract function of an abstract base class is called because a subclass has not + * implemented the function. + * @augments AbstractError + * @param {String} message The message. + */ +function UnsupportedOperationError(message) { + AbstractError.call(this, "UnsupportedOperationError", message); + + var stack; + try { + //noinspection ExceptionCaughtLocallyJS + throw new Error(); + } catch (e) { + stack = e.stack; + } + this.stack = stack; +} + +UnsupportedOperationError.prototype = Object.create(AbstractError.prototype); + +export default UnsupportedOperationError; diff --git a/web/test/WebWorldWind/src/geom/Angle.js b/web/test/WebWorldWind/src/geom/Angle.js new file mode 100644 index 00000000..8d0f7962 --- /dev/null +++ b/web/test/WebWorldWind/src/geom/Angle.js @@ -0,0 +1,216 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/** + * Provides constants and functions for working with angles. + * @exports Angle + */ + +var Angle = { + /** + * Conversion factor for degrees to radians. + * @constant + */ + DEGREES_TO_RADIANS: Math.PI / 180.0, + /** + * Conversion factor for radians to degrees. + * @constant + */ + RADIANS_TO_DEGREES: 180.0 / Math.PI, + /** + * 2 pi. + * @constant + */ + TWO_PI: 2 * Math.PI, + /** + * pi / 2 + * @constant + */ + HALF_PI: Math.PI / 2, + + /** + * Normalizes a specified value to be within the range of [-180, 180] degrees. + * @param {Number} degrees The value to normalize, in degrees. + * @returns {Number} The specified value normalized to [-180, 180] degrees. + */ + normalizedDegrees: function (degrees) { + var angle = degrees % 360; + + return angle > 180 ? angle - 360 : angle < -180 ? 360 + angle : angle; + }, + + /** + * Normalizes a specified value to be within the range of [-90, 90] degrees. + * @param {Number} degrees The value to normalize, in degrees. + * @returns {Number} The specified value normalized to the normal range of latitude. + */ + normalizedDegreesLatitude: function (degrees) { + var lat = degrees % 180; + + return lat > 90 ? 180 - lat : lat < -90 ? -180 - lat : lat; + }, + + /** + * Normalizes a specified value to be within the range of [-180, 180] degrees. + * @param {Number} degrees The value to normalize, in degrees. + * @returns {Number} The specified value normalized to the normal range of longitude. + */ + normalizedDegreesLongitude: function (degrees) { + var lon = degrees % 360; + + return lon > 180 ? lon - 360 : lon < -180 ? 360 + lon : lon; + }, + + /** + * Normalizes a specified value to be within the range of [-Pi, Pi] radians. + * @param {Number} radians The value to normalize, in radians. + * @returns {Number} The specified value normalized to [-Pi, Pi] radians. + */ + normalizedRadians: function (radians) { + var angle = radians % this.TWO_PI; + + return angle > Math.PI ? angle - this.TWO_PI : angle < -Math.PI ? this.TWO_PI + angle : angle; + }, + + /** + * Normalizes a specified value to be within the range of [-Pi/2, Pi/2] radians. + * @param {Number} radians The value to normalize, in radians. + * @returns {Number} The specified value normalized to the normal range of latitude. + */ + normalizedRadiansLatitude: function (radians) { + var lat = radians % Math.PI; + + return lat > this.HALF_PI ? Math.PI - lat : lat < -this.HALF_PI ? -Math.PI - lat : lat; + }, + + /** + * Normalizes a specified value to be within the range of [-Pi, Pi] radians. + * @param {Number} radians The value to normalize, in radians. + * @returns {Number} The specified value normalized to the normal range of longitude. + */ + normalizedRadiansLongitude: function (radians) { + var lon = radians % this.TWO_PI; + + return lon > Math.PI ? lon - this.TWO_PI : lon < -Math.PI ? this.TWO_PI + lon : lon; + }, + + /** + * Indicates whether a specified value is within the normal range of latitude, [-90, 90]. + * @param {Number} degrees The value to test, in degrees. + * @returns {Boolean} true if the value is within the normal range of latitude, otherwise false. + */ + isValidLatitude: function (degrees) { + return degrees >= -90 && degrees <= 90; + }, + + /** + * Indicates whether a specified value is within the normal range of longitude, [-180, 180]. + * @param {Number} degrees The value to test, in degrees. + * @returns {boolean} true if the value is within the normal range of longitude, otherwise false. + */ + isValidLongitude: function (degrees) { + return degrees >= -180 && degrees <= 180; + }, + + /** + * Returns a string representation of a specified value in degrees. + * @param {Number} degrees The value for which to compute the string. + * @returns {String} The computed string, which is a decimal degrees value followed by the degree symbol. + */ + toString: function (degrees) { + return degrees.toString() + '\u00B0'; + }, + + /** + * Returns a decimal degrees string representation of a specified value in degrees. + * @param {Number} degrees The value for which to compute the string. + * @returns {String} The computed string, which is a decimal degrees value followed by the degree symbol. + */ + toDecimalDegreesString: function (degrees) { + return degrees.toString() + '\u00B0'; + }, + + /** + * Returns a degrees-minutes-seconds string representation of a specified value in degrees. + * @param {Number} degrees The value for which to compute the string. + * @returns {String} The computed string in degrees, minutes and decimal seconds. + */ + toDMSString: function (degrees) { + var sign, + temp, + d, + m, + s; + + sign = degrees < 0 ? -1 : 1; + temp = sign * degrees; + d = Math.floor(temp); + temp = (temp - d) * 60; + m = Math.floor(temp); + temp = (temp - m) * 60; + s = Math.round(temp); + + if (s == 60) { + m++; + s = 0; + } + if (m == 60) { + d++; + m = 0; + } + + return (sign == -1 ? "-" : "") + d + "\u00B0" + " " + m + "\u2019" + " " + s + "\u201D"; + }, + + /** + * Returns a degrees-minutes string representation of a specified value in degrees. + * @param {Number} degrees The value for which to compute the string. + * @returns {String} The computed string in degrees and decimal minutes. + */ + toDMString: function (degrees) { + var sign, + temp, + d, + m, + s, + mf; + + sign = degrees < 0 ? -1 : 1; + temp = sign * degrees; + d = Math.floor(temp); + temp = (temp - d) * 60; + m = Math.floor(temp); + temp = (temp - m) * 60; + s = Math.round(temp); + + if (s == 60) { + m++; + s = 0; + } + if (m == 60) { + d++; + m = 0; + } + + mf = s == 0 ? m : m + s / 60; + + return (sign == -1 ? "-" : "") + d + "\u00B0" + " " + mf + "\u2019"; + } +}; + +export default Angle; diff --git a/web/test/WebWorldWind/src/geom/BoundingBox.js b/web/test/WebWorldWind/src/geom/BoundingBox.js new file mode 100644 index 00000000..1ee0d674 --- /dev/null +++ b/web/test/WebWorldWind/src/geom/BoundingBox.js @@ -0,0 +1,579 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports BoundingBox + */ +import ArgumentError from '../error/ArgumentError'; +import BasicProgram from '../shaders/BasicProgram'; +import Logger from '../util/Logger'; +import Matrix from '../geom/Matrix'; +import Vec3 from '../geom/Vec3'; +import WWMath from '../util/WWMath'; +import WWUtil from '../util/WWUtil'; + + +/** + * Constructs a unit bounding box. + * The unit box has its R, S and T axes aligned with the X, Y and Z axes, respectively, and has its length, + * width and height set to 1. + * @alias BoundingBox + * @constructor + * @classdesc Represents a bounding box in Cartesian coordinates. Typically used as a bounding volume. + */ +function BoundingBox() { + + /** + * The box's center point. + * @type {Vec3} + * @default (0, 0, 0) + */ + this.center = new Vec3(0, 0, 0); + + /** + * The center point of the box's bottom. (The origin of the R axis.) + * @type {Vec3} + * @default (-0.5, 0, 0) + */ + this.bottomCenter = new Vec3(-0.5, 0, 0); + + /** + * The center point of the box's top. (The end of the R axis.) + * @type {Vec3} + * @default (0.5, 0, 0) + */ + this.topCenter = new Vec3(0.5, 0, 0); + + /** + * The box's R axis, its longest axis. + * @type {Vec3} + * @default (1, 0, 0) + */ + this.r = new Vec3(1, 0, 0); + + /** + * The box's S axis, its mid-length axis. + * @type {Vec3} + * @default (0, 1, 0) + */ + this.s = new Vec3(0, 1, 0); + + /** + * The box's T axis, its shortest axis. + * @type {Vec3} + * @default (0, 0, 1) + */ + this.t = new Vec3(0, 0, 1); + + /** + * The box's radius. (The half-length of its diagonal.) + * @type {number} + * @default sqrt(3) + */ + this.radius = Math.sqrt(3); + + // Internal use only. Intentionally not documented. + this.tmp1 = new Vec3(0, 0, 0); + this.tmp2 = new Vec3(0, 0, 0); + this.tmp3 = new Vec3(0, 0, 0); + + // Internal use only. Intentionally not documented. + this.scratchElevations = new Float64Array(9); + this.scratchPoints = new Float64Array(3 * this.scratchElevations.length); +} + +// Internal use only. Intentionally not documented. +BoundingBox.scratchMatrix = Matrix.fromIdentity(); + +/** + * Returns the eight {@link Vec3} corners of the box. + * + * @returns {Array} 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. + */ +BoundingBox.prototype.getCorners = function () { + var ll = new Vec3(this.s[0], this.s[1], this.s[2]); + var lr = new Vec3(this.t[0], this.t[1], this.t[2]); + var ur = new Vec3(this.s[0], this.s[1], this.s[2]); + var ul = new Vec3(this.s[0], this.s[1], this.s[2]); + + ll.add(this.t).multiply(-0.5); // Lower left. + lr.subtract(this.s).multiply(0.5); // Lower right. + ur.add(this.t).multiply(0.5); // Upper right. + ul.subtract(this.t).multiply(0.5); // Upper left. + + var corners = []; + for (var i = 0; i < 4; i++) { + corners.push(new Vec3(this.bottomCenter[0], this.bottomCenter[1], this.bottomCenter[2])); + } + + for (i = 0; i < 4; i++) { + corners.push(new Vec3(this.topCenter[0], this.topCenter[1], this.topCenter[2])); + } + + corners[0].add(ll); + corners[1].add(lr); + corners[2].add(ur); + corners[3].add(ul); + corners[4].add(ll); + corners[5].add(lr); + corners[6].add(ur); + corners[7].add(ul); + + return corners; +}; + +/** + * Sets this bounding box such that it minimally encloses a specified collection of points. + * @param {Float32Array} points The points to contain. + * @returns {BoundingBox} This bounding box set to contain the specified points. + * @throws {ArgumentError} If the specified list of points is null, undefined or empty. + */ +BoundingBox.prototype.setToPoints = function (points) { + if (!points || points.length < 3) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "BoundingBox", "setToPoints", "missingArray")); + } + + var rMin = +Number.MAX_VALUE, + rMax = -Number.MAX_VALUE, + sMin = +Number.MAX_VALUE, + sMax = -Number.MAX_VALUE, + tMin = +Number.MAX_VALUE, + tMax = -Number.MAX_VALUE, + r = this.r, s = this.s, t = this.t, + p = new Vec3(0, 0, 0), + pdr, pds, pdt, rLen, sLen, tLen, rSum, sSum, tSum, + rx_2, ry_2, rz_2, cx, cy, cz; + + Matrix.principalAxesFromPoints(points, r, s, t); + + for (var i = 0, len = points.length / 3; i < len; i++) { + p[0] = points[i * 3]; + p[1] = points[i * 3 + 1]; + p[2] = points[i * 3 + 2]; + + pdr = p.dot(r); + if (rMin > pdr) + rMin = pdr; + if (rMax < pdr) + rMax = pdr; + + pds = p.dot(s); + if (sMin > pds) + sMin = pds; + if (sMax < pds) + sMax = pds; + + pdt = p.dot(t); + if (tMin > pdt) + tMin = pdt; + if (tMax < pdt) + tMax = pdt; + } + + if (rMax === rMin) + rMax = rMin + 1; + if (sMax === sMin) + sMax = sMin + 1; + if (tMax === tMin) + tMax = tMin + 1; + + rLen = rMax - rMin; + sLen = sMax - sMin; + tLen = tMax - tMin; + rSum = rMax + rMin; + sSum = sMax + sMin; + tSum = tMax + tMin; + + rx_2 = 0.5 * r[0] * rLen; + ry_2 = 0.5 * r[1] * rLen; + rz_2 = 0.5 * r[2] * rLen; + + cx = 0.5 * (r[0] * rSum + s[0] * sSum + t[0] * tSum); + cy = 0.5 * (r[1] * rSum + s[1] * sSum + t[1] * tSum); + cz = 0.5 * (r[2] * rSum + s[2] * sSum + t[2] * tSum); + + this.center[0] = cx; + this.center[1] = cy; + this.center[2] = cz; + + this.topCenter[0] = cx + rx_2; + this.topCenter[1] = cy + ry_2; + this.topCenter[2] = cz + rz_2; + + this.bottomCenter[0] = cx - rx_2; + this.bottomCenter[1] = cy - ry_2; + this.bottomCenter[2] = cz - rz_2; + + r.multiply(rLen); + s.multiply(sLen); + t.multiply(tLen); + + this.radius = 0.5 * Math.sqrt(rLen * rLen + sLen * sLen + tLen * tLen); + + return this; +}; + +/** + * Sets this bounding box such that it minimally encloses a specified collection of points. + * @param {Vec3} points The points to contain. + * @returns {BoundingBox} This bounding box set to contain the specified points. + * @throws {ArgumentError} If the specified list of points is null, undefined or empty. + */ +BoundingBox.prototype.setToVec3Points = function (points) { + if (!points || points.length === 0) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "BoundingBox", "setToVec3Points", "missingArray")); + } + + var pointList = new Float32Array(points.length * 3); + for (var i = 0; i < points.length; i++) { + var point = points[i]; + for (var j = 0; j < 3; j++) { + pointList[i * 3 + j] = point[j]; + } + } + + return this.setToPoints(pointList); +}; + +/** + * Sets this bounding box such that it contains a specified sector on a specified globe with min and max elevation. + *
+ * To create a bounding box that contains the sector at mean sea level, specify zero for the minimum and maximum + * elevations. + * To create a bounding box that contains the terrain surface in this sector, specify the actual minimum and maximum + * elevation values associated with the sector, multiplied by the model's vertical exaggeration. + * @param {Sector} sector The sector for which to create the bounding box. + * @param {Globe} globe The globe associated with the sector. + * @param {Number} minElevation The minimum elevation within the sector. + * @param {Number} maxElevation The maximum elevation within the sector. + * @returns {BoundingBox} This bounding box set to contain the specified sector. + * @throws {ArgumentError} If either the specified sector or globe is null or undefined. + */ +BoundingBox.prototype.setToSector = function (sector, globe, minElevation, maxElevation) { + if (!sector) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "BoundingBox", "setToSector", "missingSector")); + } + + if (!globe) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "BoundingBox", "setToSector", "missingGlobe")); + } + + // Compute the cartesian points for a 3x3 geographic grid. This grid captures enough detail to bound the + // sector. Use minimum elevation at the corners and max elevation everywhere else. + var elevations = this.scratchElevations, + points = this.scratchPoints; + + WWUtil.fillArray(elevations, maxElevation); + elevations[0] = elevations[2] = elevations[6] = elevations[8] = minElevation; + globe.computePointsForGrid(sector, 3, 3, elevations, Vec3.ZERO, points); + + // Compute the local coordinate axes. Since we know this box is bounding a geographic sector, we use the + // local coordinate axes at its centroid as the box axes. Using these axes results in a box that has +-10% + // the volume of a box with axes derived from a principal component analysis, but is faster to compute. + var index = 12; // index to the center point's X coordinate + this.tmp1.set(points[index], points[index + 1], points[index + 2]); + WWMath.localCoordinateAxesAtPoint(this.tmp1, globe, this.r, this.s, this.t); + + // Find the extremes along each axis. + var rExtremes = [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY], + sExtremes = [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY], + tExtremes = [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]; + + for (var i = 0, len = points.length; i < len; i += 3) { + this.tmp1.set(points[i], points[i + 1], points[i + 2]); + this.adjustExtremes(this.r, rExtremes, this.s, sExtremes, this.t, tExtremes, this.tmp1); + } + + // If the sector encompasses more than one hemisphere, the 3x3 grid does not capture enough detail to bound + // the sector. The antipodal points along the parallel through the sector's centroid represent its extremes + // in longitude. Incorporate those antipodal points into the extremes along each axis. + if (sector.deltaLongitude() > 180) { + globe.computePointFromPosition(WWMath.mercatorLat(sector.centroidLatitude()), sector.centroidLongitude() + 90, maxElevation, this.tmp1); + globe.computePointFromPosition(WWMath.mercatorLat(sector.centroidLatitude()), sector.centroidLongitude() - 90, maxElevation, this.tmp2); + this.adjustExtremes(this.r, rExtremes, this.s, sExtremes, this.t, tExtremes, this.tmp1); + this.adjustExtremes(this.r, rExtremes, this.s, sExtremes, this.t, tExtremes, this.tmp2); + } + + // Sort the axes from most prominent to least prominent. The frustum intersection methods in WWBoundingBox assume + // that the axes are defined in this way. + if (rExtremes[1] - rExtremes[0] < sExtremes[1] - sExtremes[0]) { + this.swapAxes(this.r, rExtremes, this.s, sExtremes); + } + if (sExtremes[1] - sExtremes[0] < tExtremes[1] - tExtremes[0]) { + this.swapAxes(this.s, sExtremes, this.t, tExtremes); + } + if (rExtremes[1] - rExtremes[0] < sExtremes[1] - sExtremes[0]) { + this.swapAxes(this.r, rExtremes, this.s, sExtremes); + } + + // Compute the box properties from its unit axes and the extremes along each axis. + var rLen = rExtremes[1] - rExtremes[0], + sLen = sExtremes[1] - sExtremes[0], + tLen = tExtremes[1] - tExtremes[0], + rSum = rExtremes[1] + rExtremes[0], + sSum = sExtremes[1] + sExtremes[0], + tSum = tExtremes[1] + tExtremes[0], + + cx = 0.5 * (this.r[0] * rSum + this.s[0] * sSum + this.t[0] * tSum), + cy = 0.5 * (this.r[1] * rSum + this.s[1] * sSum + this.t[1] * tSum), + cz = 0.5 * (this.r[2] * rSum + this.s[2] * sSum + this.t[2] * tSum), + rx_2 = 0.5 * this.r[0] * rLen, + ry_2 = 0.5 * this.r[1] * rLen, + rz_2 = 0.5 * this.r[2] * rLen; + + this.center.set(cx, cy, cz); + this.topCenter.set(cx + rx_2, cy + ry_2, cz + rz_2); + this.bottomCenter.set(cx - rx_2, cy - ry_2, cz - rz_2); + + this.r.multiply(rLen); + this.s.multiply(sLen); + this.t.multiply(tLen); + + this.radius = 0.5 * Math.sqrt(rLen * rLen + sLen * sLen + tLen * tLen); + + return this; +}; + +/** + * Translates this bounding box by a specified translation vector. + * @param {Vec3} translation The translation vector. + * @returns {BoundingBox} This bounding box translated by the specified translation vector. + * @throws {ArgumentError} If the specified translation vector is null or undefined. + */ +BoundingBox.prototype.translate = function (translation) { + if (!translation) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "BoundingBox", "translate", "missingVector")); + } + + this.bottomCenter.add(translation); + this.topCenter.add(translation); + this.center.add(translation); + + return this; +}; + +/** + * Computes the approximate distance between this bounding box and a specified point. + *
+ * This calculation treats the bounding box as a sphere with the same radius as the box. + * @param {Vec3} point The point to compute the distance to. + * @returns {Number} The distance from the edge of this bounding box to the specified point. + * @throws {ArgumentError} If the specified point is null or undefined. + */ +BoundingBox.prototype.distanceTo = function (point) { + if (!point) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "BoundingBox", "distanceTo", "missingPoint")); + } + + var d = this.center.distanceTo(point) - this.radius; + + return d >= 0 ? d : -d; +}; + +/** + * Computes the effective radius of this bounding box relative to a specified plane. + * @param {Plane} plane The plane of interest. + * @returns {Number} The effective radius of this bounding box to the specified plane. + * @throws {ArgumentError} If the specified plane is null or undefined. + */ +BoundingBox.prototype.effectiveRadius = function (plane) { + if (!plane) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "BoundingBox", "effectiveRadius", "missingPlane")); + } + + var n = plane.normal; + + return 0.5 * (WWMath.fabs(this.r.dot(n)) + WWMath.fabs(this.s.dot(n)) + WWMath.fabs(this.t.dot(n))); +}; + +/** + * Indicates whether this bounding box intersects a specified frustum. + * @param {Frustum} frustum The frustum of interest. + * @returns {boolean} true if the specified frustum intersects this bounding box, otherwise false. + * @throws {ArgumentError} If the specified frustum is null or undefined. + */ +BoundingBox.prototype.intersectsFrustum = function (frustum) { + if (!frustum) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "BoundingBox", "intersectsFrustum", "missingFrustum")); + } + + this.tmp1.copy(this.bottomCenter); + this.tmp2.copy(this.topCenter); + + if (this.intersectionPoint(frustum.near) < 0) { + return false; + } + if (this.intersectionPoint(frustum.far) < 0) { + return false; + } + if (this.intersectionPoint(frustum.left) < 0) { + return false; + } + if (this.intersectionPoint(frustum.right) < 0) { + return false; + } + if (this.intersectionPoint(frustum.top) < 0) { + return false; + } + if (this.intersectionPoint(frustum.bottom) < 0) { + return false; + } + + return true; +}; + +// Internal. Intentionally not documented. +BoundingBox.prototype.intersectionPoint = function (plane) { + var n = plane.normal, + effectiveRadius = 0.5 * (Math.abs(this.s.dot(n)) + Math.abs(this.t.dot(n))); + + return this.intersectsAt(plane, effectiveRadius, this.tmp1, this.tmp2); +}; + +// Internal. Intentionally not documented. +BoundingBox.prototype.intersectsAt = function (plane, effRadius, endPoint1, endPoint2) { + // Test the distance from the first end-point. + var dq1 = plane.dot(endPoint1); + var bq1 = dq1 <= -effRadius; + + // Test the distance from the second end-point. + var dq2 = plane.dot(endPoint2); + var bq2 = dq2 <= -effRadius; + + if (bq1 && bq2) { // endpoints more distant from plane than effective radius; box is on neg. side of plane + return -1; + } + + if (bq1 == bq2) { // endpoints less distant from plane than effective radius; can't draw any conclusions + return 0; + } + + // Compute and return the endpoints of the box on the positive side of the plane + this.tmp3.copy(endPoint1); + this.tmp3.subtract(endPoint2); + var t = (effRadius + dq1) / plane.normal.dot(this.tmp3); + + this.tmp3.copy(endPoint2); + this.tmp3.subtract(endPoint1); + this.tmp3.multiply(t); + this.tmp3.add(endPoint1); + + // Truncate the line to only that in the positive halfspace, e.g., inside the frustum. + if (bq1) { + endPoint1.copy(this.tmp3); + } + else { + endPoint2.copy(this.tmp3); + } + + return t; +}; + +// Internal. Intentionally not documented. +BoundingBox.prototype.adjustExtremes = function (r, rExtremes, s, sExtremes, t, tExtremes, p) { + var pdr = p.dot(r); + if (rExtremes[0] > pdr) { + rExtremes[0] = pdr; + } + if (rExtremes[1] < pdr) { + rExtremes[1] = pdr; + } + + var pds = p.dot(s); + if (sExtremes[0] > pds) { + sExtremes[0] = pds; + } + if (sExtremes[1] < pds) { + sExtremes[1] = pds; + } + + var pdt = p.dot(t); + if (tExtremes[0] > pdt) { + tExtremes[0] = pdt; + } + if (tExtremes[1] < pdt) { + tExtremes[1] = pdt; + } +}; + +// Internal. Intentionally not documented. +BoundingBox.prototype.swapAxes = function (a, aExtremes, b, bExtremes) { + a.swap(b); + + var tmp = aExtremes[0]; + aExtremes[0] = bExtremes[0]; + bExtremes[0] = tmp; + + tmp = aExtremes[1]; + aExtremes[1] = bExtremes[1]; + bExtremes[1] = tmp; +}; + +/** + * Renders this bounding box in a semi-transparent color with a highlighted outline. This function is intended + * for diagnostic use only. + * @param dc {DrawContext} dc The current draw context. + */ +BoundingBox.prototype.render = function (dc) { + var gl = dc.currentGlContext, + matrix = BoundingBox.scratchMatrix, + program = dc.findAndBindProgram(BasicProgram); + + try { + // Setup to transform unit cube coordinates to this bounding box's local coordinates, as viewed by the + // current navigator state. + matrix.copy(dc.modelviewProjection); + matrix.multiply( + this.r[0], this.s[0], this.t[0], this.center[0], + this.r[1], this.s[1], this.t[1], this.center[1], + this.r[2], this.s[2], this.t[2], this.center[2], + 0, 0, 0, 1); + matrix.multiplyByTranslation(-0.5, -0.5, -0.5); + program.loadModelviewProjection(gl, matrix); + + // Setup to draw the geometry when the eye point is inside or outside the box. + gl.disable(gl.CULL_FACE); + + // Bind the shared unit cube vertex buffer and element buffer. + gl.bindBuffer(gl.ARRAY_BUFFER, dc.unitCubeBuffer()); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, dc.unitCubeElements()); + gl.enableVertexAttribArray(program.vertexPointLocation); + gl.vertexAttribPointer(program.vertexPointLocation, 3, gl.FLOAT, false, 0, 0); + + // Draw bounding box fragments that are below the terrain. + program.loadColorComponents(gl, 0, 1, 0, 0.6); + gl.drawElements(gl.LINES, 24, gl.UNSIGNED_SHORT, 72); + program.loadColorComponents(gl, 1, 1, 1, 0.3); + gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0); + + } finally { + // Restore WorldWind's default WebGL state. + gl.enable(gl.CULL_FACE); + gl.bindBuffer(gl.ARRAY_BUFFER, null); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); + } +}; + +export default BoundingBox; diff --git a/web/test/WebWorldWind/src/geom/Frustum.js b/web/test/WebWorldWind/src/geom/Frustum.js new file mode 100644 index 00000000..17e43434 --- /dev/null +++ b/web/test/WebWorldWind/src/geom/Frustum.js @@ -0,0 +1,315 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports Frustum + */ +import ArgumentError from '../error/ArgumentError'; +import Plane from '../geom/Plane'; +import Logger from '../util/Logger'; + + +/** + * Constructs a frustum. + * @alias Frustum + * @constructor + * @classdesc Represents a six-sided view frustum in Cartesian coordinates. + * @param {Plane} left The frustum's left plane. + * @param {Plane} right The frustum's right plane. + * @param {Plane} bottom The frustum's bottom plane. + * @param {Plane} top The frustum's top plane. + * @param {Plane} near The frustum's near plane. + * @param {Plane} far The frustum's far plane. + * @throws {ArgumentError} If any specified plane is null or undefined. + */ +function Frustum(left, right, bottom, top, near, far) { + if (!left || !right || !bottom || !top || !near || !far) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Frustum", "constructor", "missingPlane")); + } + + // Internal. Intentionally not documented. See property accessors below for public interface. + this._left = left; + this._right = right; + this._bottom = bottom; + this._top = top; + this._near = near; + this._far = far; + + // Internal. Intentionally not documented. + this._planes = [this._left, this._right, this._top, this._bottom, this._near, this._far]; +} + +// These accessors are defined in order to prevent changes that would make the properties inconsistent with the +// planes array. +Object.defineProperties(Frustum.prototype, { + /** + * This frustum's left plane. + * @memberof Frustum.prototype + * @type {Plane} + * @readonly + */ + left: { + get: function () { + return this._left; + } + }, + /** + * This frustum's right plane. + * @memberof Frustum.prototype + * @type {Plane} + * @readonly + */ + right: { + get: function () { + return this._right; + } + }, + /** + * This frustum's bottom plane. + * @memberof Frustum.prototype + * @type {Plane} + * @readonly + */ + bottom: { + get: function () { + return this._bottom; + } + }, + /** + * This frustum's top plane. + * @memberof Frustum.prototype + * @type {Plane} + * @readonly + */ + top: { + get: function () { + return this._top; + } + }, + /** + * This frustum's near plane. + * @memberof Frustum.prototype + * @type {Plane} + * @readonly + */ + near: { + get: function () { + return this._near; + } + }, + /** + * This frustum's far plane. + * @memberof Frustum.prototype + * @type {Plane} + * @readonly + */ + far: { + get: function () { + return this._far; + } + } +}); + +/** + * Transforms this frustum by a specified matrix. + * @param {Matrix} matrix The matrix to apply to this frustum. + * @returns {Frustum} This frustum set to its original value multiplied by the specified matrix. + * @throws {ArgumentError} If the specified matrix is null or undefined. + */ +Frustum.prototype.transformByMatrix = function (matrix) { + if (!matrix) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Frustum", "transformByMatrix", "missingMatrix")); + } + + this._left.transformByMatrix(matrix); + this._right.transformByMatrix(matrix); + this._bottom.transformByMatrix(matrix); + this._top.transformByMatrix(matrix); + this._near.transformByMatrix(matrix); + this._far.transformByMatrix(matrix); + + return this; +}; + +/** + * Normalizes the plane vectors of the planes composing this frustum. + * @returns {Frustum} This frustum with its planes normalized. + */ +Frustum.prototype.normalize = function () { + this._left.normalize(); + this._right.normalize(); + this._bottom.normalize(); + this._top.normalize(); + this._near.normalize(); + this._far.normalize(); + + return this; +}; + +/** + * Returns a new frustum with each of its planes 1 meter from the center. + * @returns {Frustum} The new frustum. + */ +Frustum.unitFrustum = function () { + return new Frustum( + new Plane(1, 0, 0, 1), // left + new Plane(-1, 0, 0, 1), // right + new Plane(0, 1, 1, 1), // bottom + new Plane(0, -1, 0, 1), // top + new Plane(0, 0, -1, 1), // near + new Plane(0, 0, 1, 1) // far + ); +}; + +/** + * Extracts a frustum from a projection matrix. + *
+ * This method assumes that the specified matrix represents a projection matrix. If it does not represent a projection matrix + * the results are undefined. + *
+ * A projection matrix's view frustum is a Cartesian volume that contains everything visible in a scene displayed
+ * using that projection matrix.
+ *
+ * @param {Matrix} matrix The projection matrix to extract the frustum from.
+ * @return {Frustum} A new frustum containing the projection matrix's view frustum, in eye coordinates.
+ * @throws {ArgumentError} If the specified matrix is null or undefined.
+ */
+Frustum.fromProjectionMatrix = function (matrix) {
+ if (!matrix) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Frustum", "fromProjectionMatrix", "missingMatrix"));
+ }
+
+ var x, y, z, w, d, left, right, top, bottom, near, far;
+
+ // Left Plane = row 4 + row 1:
+ x = matrix[12] + matrix[0];
+ y = matrix[13] + matrix[1];
+ z = matrix[14] + matrix[2];
+ w = matrix[15] + matrix[3];
+ d = Math.sqrt(x * x + y * y + z * z); // for normalizing the coordinates
+ left = new Plane(x / d, y / d, z / d, w / d);
+
+ // Right Plane = row 4 - row 1:
+ x = matrix[12] - matrix[0];
+ y = matrix[13] - matrix[1];
+ z = matrix[14] - matrix[2];
+ w = matrix[15] - matrix[3];
+ d = Math.sqrt(x * x + y * y + z * z); // for normalizing the coordinates
+ right = new Plane(x / d, y / d, z / d, w / d);
+
+ // Bottom Plane = row 4 + row 2:
+ x = matrix[12] + matrix[4];
+ y = matrix[13] + matrix[5];
+ z = matrix[14] + matrix[6];
+ w = matrix[15] + matrix[7];
+ d = Math.sqrt(x * x + y * y + z * z); // for normalizing the coordinates
+ bottom = new Plane(x / d, y / d, z / d, w / d);
+
+ // Top Plane = row 4 - row 2:
+ x = matrix[12] - matrix[4];
+ y = matrix[13] - matrix[5];
+ z = matrix[14] - matrix[6];
+ w = matrix[15] - matrix[7];
+ d = Math.sqrt(x * x + y * y + z * z); // for normalizing the coordinates
+ top = new Plane(x / d, y / d, z / d, w / d);
+
+ // Near Plane = row 4 + row 3:
+ x = matrix[12] + matrix[8];
+ y = matrix[13] + matrix[9];
+ z = matrix[14] + matrix[10];
+ w = matrix[15] + matrix[11];
+ d = Math.sqrt(x * x + y * y + z * z); // for normalizing the coordinates
+ near = new Plane(x / d, y / d, z / d, w / d);
+
+ // Far Plane = row 4 - row 3:
+ x = matrix[12] - matrix[8];
+ y = matrix[13] - matrix[9];
+ z = matrix[14] - matrix[10];
+ w = matrix[15] - matrix[11];
+ d = Math.sqrt(x * x + y * y + z * z); // for normalizing the coordinates
+ far = new Plane(x / d, y / d, z / d, w / d);
+
+ return new Frustum(left, right, bottom, top, near, far);
+};
+
+Frustum.prototype.containsPoint = function (point) {
+ if (!point) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Frustum", "containsPoint", "missingPoint"));
+ }
+
+ // See if the point is entirely within the frustum. The dot product of the point with each plane's vector
+ // provides a distance to each plane. If this distance is less than 0, the point is clipped by that plane and
+ // neither intersects nor is contained by the space enclosed by this Frustum.
+
+ if (this._far.dot(point) <= 0)
+ return false;
+ if (this._left.dot(point) <= 0)
+ return false;
+ if (this._right.dot(point) <= 0)
+ return false;
+ if (this._top.dot(point) <= 0)
+ return false;
+ if (this._bottom.dot(point) <= 0)
+ return false;
+ if (this._near.dot(point) <= 0)
+ return false;
+
+ return true;
+};
+
+/**
+ * Determines whether a line segment intersects this frustum.
+ *
+ * @param {Vec3} pointA One end of the segment.
+ * @param {Vec3} pointB The other end of the segment.
+ *
+ * @return {boolean} true if the segment intersects or is contained in this frustum,
+ * otherwise false.
+ *
+ * @throws {ArgumentError} If either point is null or undefined.
+ */
+Frustum.prototype.intersectsSegment = function (pointA, pointB) {
+ if (!pointA || !pointB) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Frustum", "containsPoint", "missingPoint"));
+ }
+
+ // First do a trivial accept test.
+ if (this.containsPoint(pointA) || this.containsPoint(pointB))
+ return true;
+
+ if (pointA.equals(pointB))
+ return false;
+
+ for (var i = 0, len = this._planes.length; i < len; i++) {
+
+ // See if both points are behind the plane and therefore not in the frustum.
+ if (this._planes[i].onSameSide(pointA, pointB) < 0)
+ return false;
+
+ // See if the segment intersects the plane.
+ if (this._planes[i].clip(pointA, pointB) != null)
+ return true;
+ }
+
+ return false; // segment does not intersect frustum
+};
+
+export default Frustum;
diff --git a/web/test/WebWorldWind/src/geom/Line.js b/web/test/WebWorldWind/src/geom/Line.js
new file mode 100644
index 00000000..d1ad2250
--- /dev/null
+++ b/web/test/WebWorldWind/src/geom/Line.js
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports Line
+ */
+import ArgumentError from '../error/ArgumentError';
+import Logger from '../util/Logger';
+import Vec3 from '../geom/Vec3';
+
+
+/**
+ * Constructs a line from a specified origin and direction.
+ * @alias Line
+ * @constructor
+ * @classdesc Represents a line in Cartesian coordinates.
+ * @param {Vec3} origin The line's origin.
+ * @param {Vec3} direction The line's direction.
+ * @throws {ArgumentError} If either the origin or the direction are null or undefined.
+ */
+function Line(origin, direction) {
+ if (!origin) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Line", "constructor",
+ "Origin is null or undefined."));
+ }
+
+ if (!direction) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Line", "constructor",
+ "Direction is null or undefined."));
+ }
+
+ /**
+ * This line's origin.
+ * @type {Vec3}
+ */
+ this.origin = origin;
+
+ /**
+ * This line's direction.
+ * @type {Vec3}
+ */
+ this.direction = direction;
+}
+
+/**
+ * Creates a line given two specified endpoints.
+ * @param {Vec3} pointA The first endpoint.
+ * @param {Vec3} pointB The second endpoint.
+ * @return {Line} The new line.
+ * @throws {ArgumentError} If either endpoint is null or undefined.
+ */
+Line.fromSegment = function (pointA, pointB) {
+ if (!pointA || !pointB) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Line", "fromSegment", "missingPoint"));
+ }
+
+ var origin = new Vec3(pointA[0], pointA[1], pointA[2]),
+ direction = new Vec3(pointB[0] - pointA[0], pointB[1] - pointA[1], pointB[2] - pointA[2]);
+
+ return new Line(origin, direction);
+};
+
+/**
+ * Computes a Cartesian point a specified distance along this line.
+ * @param {Number} distance The distance from this line's origin at which to compute the point.
+ * @param {Vec3} result A pre-allocated {@Link Vec3} instance in which to return the computed point.
+ * @return {Vec3} The specified result argument containing the computed point.
+ * @throws {ArgumentError} If the specified result argument is null or undefined.
+ */
+Line.prototype.pointAt = function (distance, result) {
+ if (!result) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Line", "pointAt", "missingResult."));
+ }
+
+ result[0] = this.origin[0] + this.direction[0] * distance;
+ result[1] = this.origin[1] + this.direction[1] * distance;
+ result[2] = this.origin[2] + this.direction[2] * distance;
+
+ return result;
+};
+
+/**
+ * Indicates whether the components of this line are equal to those of a specified line.
+ * @param {Line} otherLine The line to test equality with. May be null or undefined, in which case this
+ * function returns false.
+ * @returns {boolean} true if all components of this line are equal to the corresponding
+ * components of the specified line, otherwise false.
+ */
+Line.prototype.equals = function (otherLine) {
+ if (otherLine) {
+ return this.origin.equals(otherLine.origin) && this.direction.equals(otherLine.direction);
+ }
+
+ return false;
+};
+
+/**
+ * Creates a new line that is a copy of this line.
+ * @returns {Line} The new line.
+ */
+Line.prototype.clone = function () {
+ var clone = new Line(new Vec3(0, 0, 0), new Vec3(0, 0, 0));
+ clone.copy(this);
+
+ return clone;
+};
+
+/**
+ * Copies the components of a specified line to this line.
+ * @param {Line} copyLine The line to copy.
+ * @returns {Line} A copy of this line equal to otherLine.
+ * @throws {ArgumentError} If the specified line is null or undefined.
+ */
+Line.prototype.copy = function (copyLine) {
+ if (!copyLine) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Line", "copy", "missingLine"));
+ }
+
+ this.origin.copy(copyLine.origin);
+ this.direction.copy(copyLine.direction);
+
+ return this;
+};
+
+export default Line;
diff --git a/web/test/WebWorldWind/src/geom/Location.js b/web/test/WebWorldWind/src/geom/Location.js
new file mode 100644
index 00000000..cdb41bdd
--- /dev/null
+++ b/web/test/WebWorldWind/src/geom/Location.js
@@ -0,0 +1,961 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports Location
+ */
+import Angle from '../geom/Angle';
+import ArgumentError from '../error/ArgumentError';
+import Logger from '../util/Logger';
+import Plane from '../geom/Plane';
+import Vec3 from '../geom/Vec3';
+import WWMath from '../util/WWMath';
+
+
+/**
+ * Constructs a location from a specified latitude and longitude in degrees.
+ * @alias Location
+ * @constructor
+ * @classdesc Represents a latitude, longitude pair in degrees.
+ * @param {Number} latitude The latitude in degrees.
+ * @param {Number} longitude The longitude in degrees.
+ */
+function Location(latitude, longitude) {
+ /**
+ * The latitude in degrees.
+ * @type {Number}
+ */
+ this.latitude = latitude;
+ /**
+ * The longitude in degrees.
+ * @type {Number}
+ */
+ this.longitude = longitude;
+}
+
+/**
+ * A location with latitude and longitude both 0.
+ * @constant
+ * @type {Location}
+ */
+Location.ZERO = new Location(0, 0);
+
+/**
+ * Creates a location from angles specified in radians.
+ * @param {Number} latitudeRadians The latitude in radians.
+ * @param {Number} longitudeRadians The longitude in radians.
+ * @returns {Location} The new location with latitude and longitude in degrees.
+ */
+Location.fromRadians = function (latitudeRadians, longitudeRadians) {
+ return new Location(latitudeRadians * Angle.RADIANS_TO_DEGREES, longitudeRadians * Angle.RADIANS_TO_DEGREES);
+};
+
+/**
+ * Copies this location to the latitude and longitude of a specified location.
+ * @param {Location} location The location to copy.
+ * @returns {Location} This location, set to the values of the specified location.
+ * @throws {ArgumentError} If the specified location is null or undefined.
+ */
+Location.prototype.copy = function (location) {
+ if (!location) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "copy", "missingLocation"));
+ }
+
+ this.latitude = location.latitude;
+ this.longitude = location.longitude;
+
+ return this;
+};
+
+/**
+ * Sets this location to the latitude and longitude.
+ * @param {Number} latitude The latitude to set.
+ * @param {Number} longitude The longitude to set.
+ * @returns {Location} This location, set to the values of the specified latitude and longitude.
+ */
+Location.prototype.set = function (latitude, longitude) {
+ this.latitude = latitude;
+ this.longitude = longitude;
+
+ return this;
+};
+
+/**
+ * Indicates whether this location is equal to a specified location.
+ * @param {Location} location The location to compare this one to.
+ * @returns {Boolean} true if this location is equal to the specified location, otherwise
+ * false.
+ */
+Location.prototype.equals = function (location) {
+ return location
+ && location.latitude === this.latitude && location.longitude === this.longitude;
+};
+
+/**
+ * Compute a location along a path at a specified distance between two specified locations.
+ * @param {String} pathType The type of path to assume. Recognized values are
+ * [WorldWind.GREAT_CIRCLE]{@link WorldWind#GREAT_CIRCLE},
+ * [WorldWind.RHUMB_LINE]{@link WorldWind#RHUMB_LINE} and
+ * [WorldWind.LINEAR]{@link WorldWind#LINEAR}.
+ * If the path type is not recognized then WorldWind.LINEAR is used.
+ * @param {Number} amount The fraction of the path between the two locations at which to compute the new
+ * location. This number should be between 0 and 1. If not, it is clamped to the nearest of those values.
+ * @param {Location} location1 The starting location.
+ * @param {Location} location2 The ending location.
+ * @param {Location} result A Location in which to return the result.
+ * @returns {Location} The specified result location.
+ * @throws {ArgumentError} If either specified location or the result argument is null or undefined.
+ */
+Location.interpolateAlongPath = function (pathType, amount, location1, location2, result) {
+ if (!location1 || !location2) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "interpolateAlongPath", "missingLocation"));
+ }
+
+ if (!result) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "interpolateAlongPath", "missingResult"));
+ }
+
+ if (pathType === WorldWind.GREAT_CIRCLE) {
+ return this.interpolateGreatCircle(amount, location1, location2, result);
+ } else if (pathType && pathType === WorldWind.RHUMB_LINE) {
+ return this.interpolateRhumb(amount, location1, location2, result);
+ } else {
+ return this.interpolateLinear(amount, location1, location2, result);
+ }
+};
+
+/**
+ * Compute a location along a great circle path at a specified distance between two specified locations.
+ * @param {Number} amount The fraction of the path between the two locations at which to compute the new
+ * location. This number should be between 0 and 1. If not, it is clamped to the nearest of those values.
+ * This function uses a spherical model, not elliptical.
+ * @param {Location} location1 The starting location.
+ * @param {Location} location2 The ending location.
+ * @param {Location} result A Location in which to return the result.
+ * @returns {Location} The specified result location.
+ * @throws {ArgumentError} If either specified location or the result argument is null or undefined.
+ */
+Location.interpolateGreatCircle = function (amount, location1, location2, result) {
+ if (!location1 || !location2) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "interpolateGreatCircle", "missingLocation"));
+ }
+ if (!result) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "interpolateGreatCircle", "missingResult"));
+ }
+
+ if (location1.equals(location2)) {
+ result.latitude = location1.latitude;
+ result.longitude = location1.longitude;
+ return result;
+ }
+
+ var t = WWMath.clamp(amount, 0, 1),
+ azimuthDegrees = this.greatCircleAzimuth(location1, location2),
+ distanceRadians = this.greatCircleDistance(location1, location2);
+
+ return this.greatCircleLocation(location1, azimuthDegrees, t * distanceRadians, result);
+};
+
+/**
+ * Computes the azimuth angle (clockwise from North) that points from the first location to the second location.
+ * This angle can be used as the starting azimuth for a great circle arc that begins at the first location, and
+ * passes through the second location.
+ * This function uses a spherical model, not elliptical.
+ * @param {Location} location1 The starting location.
+ * @param {Location} location2 The ending location.
+ * @returns {Number} The computed azimuth, in degrees.
+ * @throws {ArgumentError} If either specified location is null or undefined.
+ */
+Location.greatCircleAzimuth = function (location1, location2) {
+ if (!location1 || !location2) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "greatCircleAzimuth", "missingLocation"));
+ }
+
+ var lat1 = location1.latitude * Angle.DEGREES_TO_RADIANS,
+ lat2 = location2.latitude * Angle.DEGREES_TO_RADIANS,
+ lon1 = location1.longitude * Angle.DEGREES_TO_RADIANS,
+ lon2 = location2.longitude * Angle.DEGREES_TO_RADIANS,
+ x,
+ y,
+ azimuthRadians;
+
+ if (lat1 == lat2 && lon1 == lon2) {
+ return 0;
+ }
+
+ if (lon1 == lon2) {
+ return lat1 > lat2 ? 180 : 0;
+ }
+
+ // Taken from "Map Projections - A Working Manual", page 30, equation 5-4b.
+ // The atan2() function is used in place of the traditional atan(y/x) to simplify the case when x == 0.
+ y = Math.cos(lat2) * Math.sin(lon2 - lon1);
+ x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1);
+ azimuthRadians = Math.atan2(y, x);
+
+ return isNaN(azimuthRadians) ? 0 : azimuthRadians * Angle.RADIANS_TO_DEGREES;
+};
+
+/**
+ * Computes the great circle angular distance between two locations. The return value gives the distance as the
+ * angle between the two positions. In radians, this angle is the arc length of the segment between the two
+ * positions. To compute a distance in meters from this value, multiply the return value by the radius of the
+ * globe.
+ * This function uses a spherical model, not elliptical.
+ *
+ * @param {Location} location1 The starting location.
+ * @param {Location} location2 The ending location.
+ * @returns {Number} The computed distance, in radians.
+ * @throws {ArgumentError} If either specified location is null or undefined.
+ */
+Location.greatCircleDistance = function (location1, location2) {
+ if (!location1 || !location2) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "greatCircleDistance", "missingLocation"));
+ }
+
+ var lat1Radians = location1.latitude * Angle.DEGREES_TO_RADIANS,
+ lat2Radians = location2.latitude * Angle.DEGREES_TO_RADIANS,
+ lon1Radians = location1.longitude * Angle.DEGREES_TO_RADIANS,
+ lon2Radians = location2.longitude * Angle.DEGREES_TO_RADIANS,
+ a,
+ b,
+ c,
+ distanceRadians;
+
+ if (lat1Radians == lat2Radians && lon1Radians == lon2Radians) {
+ return 0;
+ }
+
+ // "Haversine formula," taken from https://en.wikipedia.org/wiki/Great-circle_distance#Formul.C3.A6
+ a = Math.sin((lat2Radians - lat1Radians) / 2.0);
+ b = Math.sin((lon2Radians - lon1Radians) / 2.0);
+ c = a * a + Math.cos(lat1Radians) * Math.cos(lat2Radians) * b * b;
+ distanceRadians = 2.0 * Math.asin(Math.sqrt(c));
+
+ return isNaN(distanceRadians) ? 0 : distanceRadians;
+};
+
+/**
+ * Computes the location on a great circle path corresponding to a given starting location, azimuth, and
+ * arc distance.
+ * This function uses a spherical model, not elliptical.
+ *
+ * @param {Location} location The starting location.
+ * @param {Number} greatCircleAzimuthDegrees The azimuth in degrees.
+ * @param {Number} pathLengthRadians The radian distance along the path at which to compute the end location.
+ * @param {Location} result A Location in which to return the result.
+ * @returns {Location} The specified result location.
+ * @throws {ArgumentError} If the specified location or the result argument is null or undefined.
+ */
+Location.greatCircleLocation = function (location, greatCircleAzimuthDegrees, pathLengthRadians, result) {
+ if (!location) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "greatCircleLocation", "missingLocation"));
+ }
+ if (!result) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "greatCircleLocation", "missingResult"));
+ }
+
+ if (pathLengthRadians == 0) {
+ result.latitude = location.latitude;
+ result.longitude = location.longitude;
+ return result;
+ }
+
+ var latRadians = location.latitude * Angle.DEGREES_TO_RADIANS,
+ lonRadians = location.longitude * Angle.DEGREES_TO_RADIANS,
+ azimuthRadians = greatCircleAzimuthDegrees * Angle.DEGREES_TO_RADIANS,
+ endLatRadians,
+ endLonRadians;
+
+ // Taken from "Map Projections - A Working Manual", page 31, equation 5-5 and 5-6.
+ endLatRadians = Math.asin(Math.sin(latRadians) * Math.cos(pathLengthRadians) +
+ Math.cos(latRadians) * Math.sin(pathLengthRadians) * Math.cos(azimuthRadians));
+ endLonRadians = lonRadians + Math.atan2(
+ Math.sin(pathLengthRadians) * Math.sin(azimuthRadians),
+ Math.cos(latRadians) * Math.cos(pathLengthRadians) -
+ Math.sin(latRadians) * Math.sin(pathLengthRadians) * Math.cos(azimuthRadians));
+
+ if (isNaN(endLatRadians) || isNaN(endLonRadians)) {
+ result.latitude = location.latitude;
+ result.longitude = location.longitude;
+ } else {
+ result.latitude = Angle.normalizedDegreesLatitude(endLatRadians * Angle.RADIANS_TO_DEGREES);
+ result.longitude = Angle.normalizedDegreesLongitude(endLonRadians * Angle.RADIANS_TO_DEGREES);
+ }
+
+ return result;
+};
+
+/**
+ * Compute a location along a rhumb path at a specified distance between two specified locations.
+ * This function uses a spherical model, not elliptical.
+ * @param {Number} amount The fraction of the path between the two locations at which to compute the new
+ * location. This number should be between 0 and 1. If not, it is clamped to the nearest of those values.
+ * @param {Location} location1 The starting location.
+ * @param {Location} location2 The ending location.
+ * @param {Location} result A Location in which to return the result.
+ * @returns {Location} The specified result location.
+ * @throws {ArgumentError} If either specified location or the result argument is null or undefined.
+ */
+Location.interpolateRhumb = function (amount, location1, location2, result) {
+ if (!location1 || !location2) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "interpolateRhumb", "missingLocation"));
+ }
+ if (!result) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "interpolateRhumb", "missingResult"));
+ }
+
+ if (location1.equals(location2)) {
+ result.latitude = location1.latitude;
+ result.longitude = location1.longitude;
+ return result;
+ }
+
+ var t = WWMath.clamp(amount, 0, 1),
+ azimuthDegrees = this.rhumbAzimuth(location1, location2),
+ distanceRadians = this.rhumbDistance(location1, location2);
+
+ return this.rhumbLocation(location1, azimuthDegrees, t * distanceRadians, result);
+};
+
+/**
+ * Computes the azimuth angle (clockwise from North) that points from the first location to the second location.
+ * This angle can be used as the azimuth for a rhumb arc that begins at the first location, and
+ * passes through the second location.
+ * This function uses a spherical model, not elliptical.
+ * @param {Location} location1 The starting location.
+ * @param {Location} location2 The ending location.
+ * @returns {Number} The computed azimuth, in degrees.
+ * @throws {ArgumentError} If either specified location is null or undefined.
+ */
+Location.rhumbAzimuth = function (location1, location2) {
+ if (!location1 || !location2) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "rhumbAzimuth", "missingLocation"));
+ }
+
+ var lat1 = location1.latitude * Angle.DEGREES_TO_RADIANS,
+ lat2 = location2.latitude * Angle.DEGREES_TO_RADIANS,
+ lon1 = location1.longitude * Angle.DEGREES_TO_RADIANS,
+ lon2 = location2.longitude * Angle.DEGREES_TO_RADIANS,
+ dLon,
+ dPhi,
+ azimuthRadians;
+
+ if (lat1 == lat2 && lon1 == lon2) {
+ return 0;
+ }
+
+ dLon = lon2 - lon1;
+ dPhi = Math.log(Math.tan(lat2 / 2.0 + Math.PI / 4) / Math.tan(lat1 / 2.0 + Math.PI / 4));
+
+ // If lonChange over 180 take shorter rhumb across 180 meridian.
+ if (WWMath.fabs(dLon) > Math.PI) {
+ dLon = dLon > 0 ? -(2 * Math.PI - dLon) : 2 * Math.PI + dLon;
+ }
+
+ azimuthRadians = Math.atan2(dLon, dPhi);
+
+ return isNaN(azimuthRadians) ? 0 : azimuthRadians * Angle.RADIANS_TO_DEGREES;
+};
+
+/**
+ * Computes the rhumb angular distance between two locations. The return value gives the distance as the
+ * angle between the two positions in radians. This angle is the arc length of the segment between the two
+ * positions. To compute a distance in meters from this value, multiply the return value by the radius of the
+ * globe.
+ * This function uses a spherical model, not elliptical.
+ *
+ * @param {Location} location1 The starting location.
+ * @param {Location} location2 The ending location.
+ * @returns {Number} The computed distance, in radians.
+ * @throws {ArgumentError} If either specified location is null or undefined.
+ */
+Location.rhumbDistance = function (location1, location2) {
+ if (!location1 || !location2) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "rhumbDistance", "missingLocation"));
+ }
+
+ var lat1 = location1.latitude * Angle.DEGREES_TO_RADIANS,
+ lat2 = location2.latitude * Angle.DEGREES_TO_RADIANS,
+ lon1 = location1.longitude * Angle.DEGREES_TO_RADIANS,
+ lon2 = location2.longitude * Angle.DEGREES_TO_RADIANS,
+ dLat,
+ dLon,
+ dPhi,
+ q,
+ distanceRadians;
+
+ if (lat1 == lat2 && lon1 == lon2) {
+ return 0;
+ }
+
+ dLat = lat2 - lat1;
+ dLon = lon2 - lon1;
+ dPhi = Math.log(Math.tan(lat2 / 2.0 + Math.PI / 4) / Math.tan(lat1 / 2.0 + Math.PI / 4));
+ q = dLat / dPhi;
+
+ if (isNaN(dPhi) || isNaN(q)) {
+ q = Math.cos(lat1);
+ }
+
+ // If lonChange over 180 take shorter rhumb across 180 meridian.
+ if (WWMath.fabs(dLon) > Math.PI) {
+ dLon = dLon > 0 ? -(2 * Math.PI - dLon) : 2 * Math.PI + dLon;
+ }
+
+ distanceRadians = Math.sqrt(dLat * dLat + q * q * dLon * dLon);
+
+ return isNaN(distanceRadians) ? 0 : distanceRadians;
+};
+
+/**
+ * Computes the location on a rhumb arc with the given starting location, azimuth, and arc distance.
+ * This function uses a spherical model, not elliptical.
+ *
+ * @param {Location} location The starting location.
+ * @param {Number} azimuthDegrees The azimuth in degrees.
+ * @param {Number} pathLengthRadians The radian distance along the path at which to compute the location.
+ * @param {Location} result A Location in which to return the result.
+ * @returns {Location} The specified result location.
+ * @throws {ArgumentError} If the specified location or the result argument is null or undefined.
+ */
+Location.rhumbLocation = function (location, azimuthDegrees, pathLengthRadians, result) {
+ if (!location) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "rhumbLocation", "missingLocation"));
+ }
+ if (!result) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "rhumbLocation", "missingResult"));
+ }
+
+ if (pathLengthRadians == 0) {
+ result.latitude = location.latitude;
+ result.longitude = location.longitude;
+ return result;
+ }
+
+ var latRadians = location.latitude * Angle.DEGREES_TO_RADIANS,
+ lonRadians = location.longitude * Angle.DEGREES_TO_RADIANS,
+ azimuthRadians = azimuthDegrees * Angle.DEGREES_TO_RADIANS,
+ endLatRadians = latRadians + pathLengthRadians * Math.cos(azimuthRadians),
+ dPhi = Math.log(Math.tan(endLatRadians / 2 + Math.PI / 4) / Math.tan(latRadians / 2 + Math.PI / 4)),
+ q = (endLatRadians - latRadians) / dPhi,
+ dLon,
+ endLonRadians;
+
+ if (isNaN(dPhi) || isNaN(q) || !isFinite(q)) {
+ q = Math.cos(latRadians);
+ }
+
+ dLon = pathLengthRadians * Math.sin(azimuthRadians) / q;
+
+ // Handle latitude passing over either pole.
+ if (WWMath.fabs(endLatRadians) > Math.PI / 2) {
+ endLatRadians = endLatRadians > 0 ? Math.PI - endLatRadians : -Math.PI - endLatRadians;
+ }
+
+ endLonRadians = WWMath.fmod(lonRadians + dLon + Math.PI, 2 * Math.PI) - Math.PI;
+
+ if (isNaN(endLatRadians) || isNaN(endLonRadians)) {
+ result.latitude = location.latitude;
+ result.longitude = location.longitude;
+ } else {
+ result.latitude = Angle.normalizedDegreesLatitude(endLatRadians * Angle.RADIANS_TO_DEGREES);
+ result.longitude = Angle.normalizedDegreesLongitude(endLonRadians * Angle.RADIANS_TO_DEGREES);
+ }
+
+ return result;
+};
+
+/**
+ * Compute a location along a linear path at a specified distance between two specified locations.
+ * @param {Number} amount The fraction of the path between the two locations at which to compute the new
+ * location. This number should be between 0 and 1. If not, it is clamped to the nearest of those values.
+ * @param {Location} location1 The starting location.
+ * @param {Location} location2 The ending location.
+ * @param {Location} result A Location in which to return the result.
+ * @returns {Location} The specified result location.
+ * @throws {ArgumentError} If either specified location or the result argument is null or undefined.
+ */
+Location.interpolateLinear = function (amount, location1, location2, result) {
+ if (!location1 || !location2) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "interpolateLinear", "missingLocation"));
+ }
+ if (!result) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "interpolateLinear", "missingResult"));
+ }
+
+ if (location1.equals(location2)) {
+ result.latitude = location1.latitude;
+ result.longitude = location1.longitude;
+ return result;
+ }
+
+ var t = WWMath.clamp(amount, 0, 1),
+ azimuthDegrees = this.linearAzimuth(location1, location2),
+ distanceRadians = this.linearDistance(location1, location2);
+
+ return this.linearLocation(location1, azimuthDegrees, t * distanceRadians, result);
+};
+
+/**
+ * Computes the azimuth angle (clockwise from North) that points from the first location to the second location.
+ * This angle can be used as the azimuth for a linear arc that begins at the first location, and
+ * passes through the second location.
+ * @param {Location} location1 The starting location.
+ * @param {Location} location2 The ending location.
+ * @returns {Number} The computed azimuth, in degrees.
+ * @throws {ArgumentError} If either specified location is null or undefined.
+ */
+Location.linearAzimuth = function (location1, location2) {
+ if (!location1 || !location2) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "linearAzimuth", "missingLocation"));
+ }
+
+ var lat1 = location1.latitude * Angle.DEGREES_TO_RADIANS,
+ lat2 = location2.latitude * Angle.DEGREES_TO_RADIANS,
+ lon1 = location1.longitude * Angle.DEGREES_TO_RADIANS,
+ lon2 = location2.longitude * Angle.DEGREES_TO_RADIANS,
+ dLon,
+ dPhi,
+ azimuthRadians;
+
+ if (lat1 == lat2 && lon1 == lon2) {
+ return 0;
+ }
+
+ dLon = lon2 - lon1;
+ dPhi = lat2 - lat1;
+
+ // If longitude change is over 180 take shorter path across 180 meridian.
+ if (WWMath.fabs(dLon) > Math.PI) {
+ dLon = dLon > 0 ? -(2 * Math.PI - dLon) : 2 * Math.PI + dLon;
+ }
+
+ azimuthRadians = Math.atan2(dLon, dPhi);
+
+ return isNaN(azimuthRadians) ? 0 : azimuthRadians * Angle.RADIANS_TO_DEGREES;
+};
+
+/**
+ * Computes the linear angular distance between two locations. The return value gives the distance as the
+ * angle between the two positions in radians. This angle is the arc length of the segment between the two
+ * positions. To compute a distance in meters from this value, multiply the return value by the radius of the
+ * globe.
+ *
+ * @param {Location} location1 The starting location.
+ * @param {Location} location2 The ending location.
+ * @returns {Number} The computed distance, in radians.
+ * @throws {ArgumentError} If either specified location is null or undefined.
+ */
+Location.linearDistance = function (location1, location2) {
+ if (!location1 || !location2) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "linearDistance", "missingLocation"));
+ }
+
+ var lat1 = location1.latitude * Angle.DEGREES_TO_RADIANS,
+ lat2 = location2.latitude * Angle.DEGREES_TO_RADIANS,
+ lon1 = location1.longitude * Angle.DEGREES_TO_RADIANS,
+ lon2 = location2.longitude * Angle.DEGREES_TO_RADIANS,
+ dLat,
+ dLon,
+ distanceRadians;
+
+ if (lat1 == lat2 && lon1 == lon2) {
+ return 0;
+ }
+
+ dLat = lat2 - lat1;
+ dLon = lon2 - lon1;
+
+ // If lonChange over 180 take shorter path across 180 meridian.
+ if (WWMath.fabs(dLon) > Math.PI) {
+ dLon = dLon > 0 ? -(2 * Math.PI - dLon) : 2 * Math.PI + dLon;
+ }
+
+ distanceRadians = Math.sqrt(dLat * dLat + dLon * dLon);
+
+ return isNaN(distanceRadians) ? 0 : distanceRadians;
+};
+
+/**
+ * Computes the location on a linear path with the given starting location, azimuth, and arc distance.
+ *
+ * @param {Location} location The starting location.
+ * @param {Number} azimuthDegrees The azimuth in degrees.
+ * @param {Number} pathLengthRadians The radian distance along the path at which to compute the location.
+ * @param {Location} result A Location in which to return the result.
+ * @returns {Location} The specified result location.
+ * @throws {ArgumentError} If the specified location or the result argument is null or undefined.
+ */
+Location.linearLocation = function (location, azimuthDegrees, pathLengthRadians, result) {
+ if (!location) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "linearLocation", "missingLocation"));
+ }
+ if (!result) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "linearLocation", "missingResult"));
+ }
+
+ if (pathLengthRadians == 0) {
+ result.latitude = location.latitude;
+ result.longitude = location.longitude;
+ return result;
+ }
+
+ var latRadians = location.latitude * Angle.DEGREES_TO_RADIANS,
+ lonRadians = location.longitude * Angle.DEGREES_TO_RADIANS,
+ azimuthRadians = azimuthDegrees * Angle.DEGREES_TO_RADIANS,
+ endLatRadians = latRadians + pathLengthRadians * Math.cos(azimuthRadians),
+ endLonRadians;
+
+ // Handle latitude passing over either pole.
+ if (WWMath.fabs(endLatRadians) > Math.PI / 2) {
+ endLatRadians = endLatRadians > 0 ? Math.PI - endLatRadians : -Math.PI - endLatRadians;
+ }
+
+ endLonRadians =
+ WWMath.fmod(lonRadians + pathLengthRadians * Math.sin(azimuthRadians) + Math.PI, 2 * Math.PI) - Math.PI;
+
+ if (isNaN(endLatRadians) || isNaN(endLonRadians)) {
+ result.latitude = location.latitude;
+ result.longitude = location.longitude;
+ } else {
+ result.latitude = Angle.normalizedDegreesLatitude(endLatRadians * Angle.RADIANS_TO_DEGREES);
+ result.longitude = Angle.normalizedDegreesLongitude(endLonRadians * Angle.RADIANS_TO_DEGREES);
+ }
+
+ return result;
+};
+
+/**
+ * Determine whether a list of locations crosses the dateline.
+ * @param {Location[]} locations The locations to test.
+ * @returns {boolean} True if the dateline is crossed, else false.
+ * @throws {ArgumentError} If the locations list is null.
+ */
+Location.locationsCrossDateLine = function (locations) {
+ if (!locations) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "locationsCrossDateline", "missingLocation"));
+ }
+
+ var pos = null;
+ for (var idx = 0, len = locations.length; idx < len; idx += 1) {
+ var posNext = locations[idx];
+
+ if (pos != null) {
+ // A segment cross the line if end pos have different longitude signs
+ // and are more than 180 degrees longitude apart
+ if (WWMath.signum(pos.longitude) != WWMath.signum(posNext.longitude)) {
+ var delta = Math.abs(pos.longitude - posNext.longitude);
+ if (delta > 180 && delta < 360)
+ return true;
+ }
+ }
+ pos = posNext;
+ }
+
+ return false;
+};
+
+/**
+ * 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 {Location[]} locations The pairs of locations defining a sequence of great circle arcs.
+ *
+ * @return {Location[]} Two locations with the most extreme latitudes on the great circle arcs.
+ *
+ * @throws IllegalArgumentException if locations is null.
+ */
+Location.greatCircleArcExtremeLocations = function (locations) {
+ if (!locations) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "greatCircleArcExtremeLocations", "missingLocation"));
+ }
+
+ var minLatLocation = null;
+ var maxLatLocation = null;
+
+ var lastLocation = null;
+
+ for (var idx = 0, len = locations.length; idx < len; idx += 1) {
+ var location = locations[idx];
+
+ if (lastLocation != null) {
+ var extremes = Location.greatCircleArcExtremeForTwoLocations(lastLocation, location);
+ if (extremes == null) {
+ continue;
+ }
+
+ if (minLatLocation == null || minLatLocation.latitude > extremes[0].latitude) {
+ minLatLocation = extremes[0];
+ }
+ if (maxLatLocation == null || maxLatLocation.latitude < extremes[1].latitude) {
+ maxLatLocation = extremes[1];
+ }
+ }
+
+ lastLocation = location;
+ }
+
+ return [minLatLocation, maxLatLocation];
+};
+
+/**
+ * Returns two locations with the most extreme latitudes on the great circle arc defined by, and limited to, the two
+ * locations.
+ *
+ * @param {Location} begin Beginning location on the great circle arc.
+ * @param {Location} end Ending location on the great circle arc.
+ *
+ * @return {Location[]} Two locations with the most extreme latitudes on the great circle arc.
+ *
+ * @throws {ArgumentError} If either begin or end are null.
+ */
+Location.greatCircleArcExtremeForTwoLocations = function (begin, end) {
+ if (!begin || !end) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "greatCircleArcExtremeForTwoLocations", "missingLocation"));
+ }
+
+ var idx, len, location; // Iteration variables.
+ var minLatLocation = null;
+ var maxLatLocation = null;
+ var minLat = 90;
+ var maxLat = -90;
+
+ // Compute the min and max latitude and associated locations from the arc endpoints.
+ var locations = [begin, end];
+ for (idx = 0, len = locations.length; idx < len; idx += 1) {
+ location = locations[idx];
+
+ if (minLat >= location.latitude) {
+ minLat = location.latitude;
+ minLatLocation = location;
+ }
+ if (maxLat <= location.latitude) {
+ maxLat = location.latitude;
+ maxLatLocation = location;
+ }
+ }
+ // The above could be written for greater clarity, simplicity, and speed:
+ // minLat = Math.min(begin.latitude, end.latitude);
+ // maxLat = Math.max(begin.latitude, end.latitude);
+ // minLatLocation = minLat == begin.latitude ? begin : end;
+ // maxLatLocation = maxLat == begin.latitude ? begin : end;
+
+ // 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.
+ var greatArcAzimuth = Location.greatCircleAzimuth(begin, end);
+ var greatArcDistance = Location.greatCircleDistance(begin, end);
+ var greatCircleExtremes = Location.greatCircleExtremeLocationsUsingAzimuth(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 (idx = 0, len = greatCircleExtremes.length; idx < len; idx += 1) {
+ location = greatCircleExtremes[idx];
+
+ var az = Location.greatCircleAzimuth(begin, location);
+ var d = Location.greatCircleDistance(begin, location);
+
+ // 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 (WWMath.signum(az) == WWMath.signum(greatArcAzimuth)) {
+ if (d >= 0 && d <= greatArcDistance) {
+ if (minLat >= location.latitude) {
+ minLat = location.latitude;
+ minLatLocation = location;
+ }
+ if (maxLat <= location.latitude) {
+ maxLat = location.latitude;
+ maxLatLocation = location;
+ }
+ }
+ }
+ }
+
+ return [minLatLocation, maxLatLocation];
+};
+
+/**
+ * Returns two locations with the most extreme latitudes on the great circle with the given starting location and
+ * azimuth.
+ *
+ * @param {Location} location Location on the great circle.
+ * @param {number} azimuth Great circle azimuth angle (clockwise from North).
+ *
+ * @return {Location[]} Two locations where the great circle has its extreme latitudes.
+ *
+ * @throws {ArgumentError} If location is null.
+ */
+Location.greatCircleExtremeLocationsUsingAzimuth = function (location, azimuth) {
+ if (!location) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Location", "greatCircleArcExtremeLocationsUsingAzimuth", "missingLocation"));
+ }
+
+ var lat0 = location.latitude;
+ var az = azimuth * Angle.DEGREES_TO_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
+
+ var tanDistance = -Math.tan(lat0) / Math.cos(az);
+ var distance = Math.atan(tanDistance);
+
+ var extremeDistance1 = distance + Math.PI / 2.0;
+ var extremeDistance2 = distance - Math.PI / 2.0;
+
+ return [
+ Location.greatCircleLocation(location, azimuth, extremeDistance1, new Location(0, 0)),
+ Location.greatCircleLocation(location, azimuth, extremeDistance2, new Location(0, 0))
+ ];
+};
+
+/**
+ * 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 {Location} p1 First position.
+ * @param {Location} p2 Second position.
+ * @param {number} meridian Longitude line to intersect with.
+ * @param {Globe} globe Globe used to compute intersection.
+ *
+ * @return {number} latitude The intersection latitude along the meridian
+ *
+ * TODO: this code allocates 4 new Vec3 and 1 new Position; use scratch variables???
+ * TODO: Why not? Every location created would then allocated those variables as well, even if they aren't needed :(.
+ */
+Location.intersectionWithMeridian = function (p1, p2, meridian, globe) {
+ // TODO: add support for 2D
+ //if (globe instanceof Globe2D)
+ //{
+ // // y = mx + b case after normalizing negative angles.
+ // double lon1 = p1.getLongitude().degrees < 0 ? p1.getLongitude().degrees + 360 : p1.getLongitude().degrees;
+ // double lon2 = p2.getLongitude().degrees < 0 ? p2.getLongitude().degrees + 360 : p2.getLongitude().degrees;
+ // if (lon1 == lon2)
+ // return null;
+ //
+ // double med = meridian.degrees < 0 ? meridian.degrees + 360 : meridian.degrees;
+ // double slope = (p2.latitude.degrees - p1.latitude.degrees) / (lon2 - lon1);
+ // double lat = p1.latitude.degrees + slope * (med - lon1);
+ //
+ // return LatLon.fromDegrees(lat, meridian.degrees);
+ //}
+
+ var pt1 = globe.computePointFromLocation(p1.latitude, p1.longitude, new Vec3(0, 0, 0));
+ var pt2 = globe.computePointFromLocation(p2.latitude, p2.longitude, new Vec3(0, 0, 0));
+
+ // Compute a plane through the origin, North Pole, and the desired meridian.
+ var northPole = globe.computePointFromLocation(90, meridian, new Vec3(0, 0, 0));
+ var pointOnEquator = globe.computePointFromLocation(0, meridian, new Vec3(0, 0, 0));
+
+ var plane = Plane.fromPoints(northPole, pointOnEquator, Vec3.ZERO);
+
+ var intersectionPoint = new Vec3(0, 0, 0);
+ if (!plane.intersectsSegmentAt(pt1, pt2, intersectionPoint)) {
+ return null;
+ }
+
+ // TODO: unable to simply create a new Position(0, 0, 0)
+ var pos = new WorldWind.Position(0, 0, 0);
+ globe.computePositionFromPoint(intersectionPoint[0], intersectionPoint[1], intersectionPoint[2], pos);
+
+ return pos.latitude;
+};
+
+/**
+ * Determine where a line between two positions crosses a given meridian. The intersection test is performed by
+ * intersecting a line in Cartesian space. 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 {Location | Position} p1 First position.
+ * @param {Location | Position} p2 Second position.
+ * @param {number} meridian Longitude line to intersect with.
+ *
+ * @return {number | null} latitude The intersection latitude along the meridian
+ * or null if the line is collinear with the meridian
+ */
+Location.meridianIntersection = function (p1, p2, meridian) {
+ // y = mx + b case after normalizing negative angles.
+ var lon1 = p1.longitude < 0 ? p1.longitude + 360 : p1.longitude;
+ var lon2 = p2.longitude < 0 ? p2.longitude + 360 : p2.longitude;
+ if (lon1 === lon2) {
+ //infinite solutions, the line is collinear with the anti-meridian
+ return null;
+ }
+
+ var med = meridian < 0 ? meridian + 360 : meridian;
+ var slope = (p2.latitude - p1.latitude) / (lon2 - lon1);
+ var lat = p1.latitude + slope * (med - lon1);
+
+ return lat;
+};
+
+/**
+ * A bit mask indicating which if any pole is being referenced.
+ * This corresponds to Java WW's AVKey.NORTH and AVKey.SOUTH,
+ * although this encoding can capture both poles simultaneously, which was
+ * a 'to do' item in the Java implementation.
+ * @type {{NONE: number, NORTH: number, SOUTH: number}}
+ */
+Location.poles = {
+ 'NONE': 0,
+ 'NORTH': 1,
+ 'SOUTH': 2
+};
+
+export default Location;
diff --git a/web/test/WebWorldWind/src/geom/Matrix.js b/web/test/WebWorldWind/src/geom/Matrix.js
new file mode 100644
index 00000000..b2f29e2a
--- /dev/null
+++ b/web/test/WebWorldWind/src/geom/Matrix.js
@@ -0,0 +1,1908 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports Matrix
+ */
+import Angle from '../geom/Angle';
+import ArgumentError from '../error/ArgumentError';
+import Logger from '../util/Logger';
+import Position from '../geom/Position';
+import Vec3 from '../geom/Vec3';
+import WWMath from '../util/WWMath';
+
+
+/**
+ * Constructs a matrix.
+ * @alias Matrix
+ * @constructor
+ * @classdesc Represents a 4 x 4 double precision matrix stored in a Float64Array in row-major order.
+ * @param {Number} m11 matrix element at row 1, column 1.
+ * @param {Number} m12 matrix element at row 1, column 2.
+ * @param {Number} m13 matrix element at row 1, column 3.
+ * @param {Number} m14 matrix element at row 1, column 4.
+ * @param {Number} m21 matrix element at row 2, column 1.
+ * @param {Number} m22 matrix element at row 2, column 2.
+ * @param {Number} m23 matrix element at row 2, column 3.
+ * @param {Number} m24 matrix element at row 2, column 4.
+ * @param {Number} m31 matrix element at row 3, column 1.
+ * @param {Number} m32 matrix element at row 3, column 2.
+ * @param {Number} m33 matrix element at row 3, column 3.
+ * @param {Number} m34 matrix element at row 3, column 4.
+ * @param {Number} m41 matrix element at row 4, column 1.
+ * @param {Number} m42 matrix element at row 4, column 2.
+ * @param {Number} m43 matrix element at row 4, column 3.
+ * @param {Number} m44 matrix element at row 4, column 4.
+ */
+function Matrix(m11, m12, m13, m14,
+ m21, m22, m23, m24,
+ m31, m32, m33, m34,
+ m41, m42, m43, m44) {
+ this[0] = m11;
+ this[1] = m12;
+ this[2] = m13;
+ this[3] = m14;
+ this[4] = m21;
+ this[5] = m22;
+ this[6] = m23;
+ this[7] = m24;
+ this[8] = m31;
+ this[9] = m32;
+ this[10] = m33;
+ this[11] = m34;
+ this[12] = m41;
+ this[13] = m42;
+ this[14] = m43;
+ this[15] = m44;
+}
+
+// Derives from Float64Array.
+Matrix.prototype = new Float64Array(16);
+
+/**
+ * Creates an identity matrix.
+ * @returns {Matrix} A new identity matrix.
+ */
+Matrix.fromIdentity = function () {
+ return new Matrix(
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, 0, 1
+ );
+};
+
+/**
+ * Computes the principal axes of a point collection expressed in a typed array.
+ * @param {Float32Array} points The points for which to compute the axes,
+ * expressed as X0, Y0, Z0, X1, Y1, Z1, ...
+ * @param {Vec3} axis1 A vector in which to return the first (longest) principal axis.
+ * @param {Vec3} axis2 A vector in which to return the second (mid-length) principal axis.
+ * @param {Vec3} axis3 A vector in which to return the third (shortest) principal axis.
+ * @throws {ArgumentError} If the specified points array is null, undefined or empty, or one of the
+ * specified axes arguments is null or undefined.
+ */
+Matrix.principalAxesFromPoints = function (points, axis1, axis2, axis3) {
+ if (!points || points.length < 1) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "principalAxesFromPoints",
+ "missingPoints"));
+ }
+
+ if (!axis1 || !axis2 || !axis3) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "principalAxesFromPoints",
+ "An axis argument is null or undefined."));
+ }
+
+ // Compute the covariance matrix.
+ var covariance = Matrix.fromIdentity();
+ covariance.setToCovarianceOfPoints(points);
+
+ // Compute the eigenvectors from the covariance matrix. Since the covariance matrix is symmetric by
+ // definition, we can safely use the "symmetric" method below.
+ covariance.eigensystemFromSymmetricMatrix(axis1, axis2, axis3);
+
+ // Normalize the eigenvectors, which are already sorted in order from most prominent to least prominent.
+ axis1.normalize();
+ axis2.normalize();
+ axis3.normalize();
+};
+
+/**
+ * Sets the components of this matrix to specified values.
+ * @param {Number} m11 matrix element at row 1, column 1.
+ * @param {Number} m12 matrix element at row 1, column 2.
+ * @param {Number} m13 matrix element at row 1, column 3.
+ * @param {Number} m14 matrix element at row 1, column 4.
+ * @param {Number} m21 matrix element at row 2, column 1.
+ * @param {Number} m22 matrix element at row 2, column 2.
+ * @param {Number} m23 matrix element at row 2, column 3.
+ * @param {Number} m24 matrix element at row 2, column 4.
+ * @param {Number} m31 matrix element at row 3, column 1.
+ * @param {Number} m32 matrix element at row 3, column 2.
+ * @param {Number} m33 matrix element at row 3, column 3.
+ * @param {Number} m34 matrix element at row 3, column 4.
+ * @param {Number} m41 matrix element at row 4, column 1.
+ * @param {Number} m42 matrix element at row 4, column 2.
+ * @param {Number} m43 matrix element at row 4, column 3.
+ * @param {Number} m44 matrix element at row 4, column 4.
+ * @returns {Matrix} This matrix with its components set to the specified values.
+ */
+Matrix.prototype.set = function (m11, m12, m13, m14,
+ m21, m22, m23, m24,
+ m31, m32, m33, m34,
+ m41, m42, m43, m44) {
+ this[0] = m11;
+ this[1] = m12;
+ this[2] = m13;
+ this[3] = m14;
+ this[4] = m21;
+ this[5] = m22;
+ this[6] = m23;
+ this[7] = m24;
+ this[8] = m31;
+ this[9] = m32;
+ this[10] = m33;
+ this[11] = m34;
+ this[12] = m41;
+ this[13] = m42;
+ this[14] = m43;
+ this[15] = m44;
+
+ return this;
+};
+
+/**
+ * Sets this matrix to the identity matrix.
+ * @returns {Matrix} This matrix set to the identity matrix.
+ */
+Matrix.prototype.setToIdentity = function () {
+ this[0] = 1;
+ this[1] = 0;
+ this[2] = 0;
+ this[3] = 0;
+ this[4] = 0;
+ this[5] = 1;
+ this[6] = 0;
+ this[7] = 0;
+ this[8] = 0;
+ this[9] = 0;
+ this[10] = 1;
+ this[11] = 0;
+ this[12] = 0;
+ this[13] = 0;
+ this[14] = 0;
+ this[15] = 1;
+};
+
+/**
+ * Copies the components of a specified matrix to this matrix.
+ * @param {Matrix} matrix The matrix to copy.
+ * @returns {Matrix} This matrix set to the values of the specified matrix.
+ * @throws {ArgumentError} If the specified matrix is null or undefined.
+ */
+Matrix.prototype.copy = function (matrix) {
+ if (!matrix) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "copy", "missingMatrix"));
+ }
+
+ this[0] = matrix[0];
+ this[1] = matrix[1];
+ this[2] = matrix[2];
+ this[3] = matrix[3];
+ this[4] = matrix[4];
+ this[5] = matrix[5];
+ this[6] = matrix[6];
+ this[7] = matrix[7];
+ this[8] = matrix[8];
+ this[9] = matrix[9];
+ this[10] = matrix[10];
+ this[11] = matrix[11];
+ this[12] = matrix[12];
+ this[13] = matrix[13];
+ this[14] = matrix[14];
+ this[15] = matrix[15];
+};
+
+/**
+ * Creates a new matrix that is a copy of this matrix.
+ * @returns {Matrix} The new matrix.
+ */
+Matrix.prototype.clone = function () {
+ var clone = Matrix.fromIdentity();
+ clone.copy(this);
+
+ return clone;
+};
+
+/**
+ * Indicates whether the components of this matrix are equal to those of a specified matrix.
+ * @param {Matrix} matrix The matrix to test equality with. May be null or undefined, in which case this
+ * function returns false.
+ * @returns {boolean} true if all components of this matrix are equal to the corresponding
+ * components of the specified matrix, otherwise false.
+ */
+Matrix.prototype.equals = function (matrix) {
+ return matrix
+ && this[0] == matrix[0]
+ && this[1] == matrix[1]
+ && this[2] == matrix[2]
+ && this[3] == matrix[3]
+ && this[4] == matrix[4]
+ && this[5] == matrix[5]
+ && this[6] == matrix[6]
+ && this[7] == matrix[7]
+ && this[8] == matrix[8]
+ && this[9] == matrix[9]
+ && this[10] == matrix[10]
+ && this[11] == matrix[11]
+ && this[12] == matrix[12]
+ && this[13] == matrix[13]
+ && this[14] == matrix[14]
+ && this[15] == matrix[15];
+};
+
+/**
+ * Stores this matrix's components in column-major order in a specified array.
+ *
+ * The array must have space for at least 16 elements. This matrix's components are stored in the array + * starting with row 0 column 0 in index 0, row 1 column 0 in index 1, row 2 column 0 in index 2, and so on. + * + * @param {Float32Array | Float64Array | Number[]} result An array of at least 16 elements. Upon return, + * contains this matrix's components in column-major. + * @returns {Float32Array} The specified result array. + * @throws {ArgumentError} If the specified result array in null or undefined. + */ +Matrix.prototype.columnMajorComponents = function (result) { + if (!result) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "columnMajorComponents", "missingResult")); + } + + // Column 1 + result[0] = this[0]; + result[1] = this[4]; + result[2] = this[8]; + result[3] = this[12]; + // Column 2 + result[4] = this[1]; + result[5] = this[5]; + result[6] = this[9]; + result[7] = this[13]; + // Column 3 + result[8] = this[2]; + result[9] = this[6]; + result[10] = this[10]; + result[11] = this[14]; + // Column 4 + result[12] = this[3]; + result[13] = this[7]; + result[14] = this[11]; + result[15] = this[15]; + + return result; +}; + +/** + * Sets this matrix to a translation matrix with specified translation components. + * @param {Number} x The X translation component. + * @param {Number} y The Y translation component. + * @param {Number} z The Z translation component. + * @returns {Matrix} This matrix with its translation components set to those specified and all other + * components set to that of an identity matrix. + */ +Matrix.prototype.setToTranslation = function (x, y, z) { + this[0] = 1; + this[1] = 0; + this[2] = 0; + this[3] = x; + this[4] = 0; + this[5] = 1; + this[6] = 0; + this[7] = y; + this[8] = 0; + this[9] = 0; + this[10] = 1; + this[11] = z; + this[12] = 0; + this[13] = 0; + this[14] = 0; + this[15] = 1; + + return this; +}; + +/** + * Sets the translation components of this matrix to specified values. + * @param {Number} x The X translation component. + * @param {Number} y The Y translation component. + * @param {Number} z The Z translation component. + * @returns {Matrix} This matrix with its translation components set to the specified values and all other + * components unmodified. + */ +Matrix.prototype.setTranslation = function (x, y, z) { + this[3] = x; + this[7] = y; + this[11] = z; + + return this; +}; + +/** + * Sets this matrix to a scale matrix with specified scale components. + * @param {Number} xScale The X scale component. + * @param {Number} yScale The Y scale component. + * @param {Number} zScale The Z scale component. + * @returns {Matrix} This matrix with its scale components set to those specified and all other + * components set to that of an identity matrix. + */ +Matrix.prototype.setToScale = function (xScale, yScale, zScale) { + this[0] = xScale; + this[1] = 0; + this[2] = 0; + this[3] = 0; + this[4] = 0; + this[5] = yScale; + this[6] = 0; + this[7] = 0; + this[8] = 0; + this[9] = 0; + this[10] = zScale; + this[11] = 0; + this[12] = 0; + this[13] = 0; + this[14] = 0; + this[15] = 1; + + return this; +}; + +/** + * Sets the scale components of this matrix to specified values. + * @param {Number} xScale The X scale component. + * @param {Number} yScale The Y scale component. + * @param {Number} zScale The Z scale component. + * @returns {Matrix} This matrix with its scale components set to the specified values and all other + * components unmodified. + */ +Matrix.prototype.setScale = function (xScale, yScale, zScale) { + this[0] = xScale; + this[5] = yScale; + this[10] = zScale; + + return this; +}; + +/** + * Sets this matrix to the transpose of a specified matrix. + * @param {Matrix} matrix The matrix whose transpose is to be copied. + * @returns {Matrix} This matrix, with its values set to the transpose of the specified matrix. + * @throws {ArgumentError} If the specified matrix in null or undefined. + */ +Matrix.prototype.setToTransposeOfMatrix = function (matrix) { + if (!matrix) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "setToTransposeOfMatrix", "missingMatrix")); + } + + this[0] = matrix[0]; + this[1] = matrix[4]; + this[2] = matrix[8]; + this[3] = matrix[12]; + this[4] = matrix[1]; + this[5] = matrix[5]; + this[6] = matrix[9]; + this[7] = matrix[13]; + this[8] = matrix[2]; + this[9] = matrix[6]; + this[10] = matrix[10]; + this[11] = matrix[14]; + this[12] = matrix[3]; + this[13] = matrix[7]; + this[14] = matrix[11]; + this[15] = matrix[15]; + + return this; +}; + +/** + * Sets this matrix to the matrix product of two specified matrices. + * @param {Matrix} matrixA The first matrix multiplicand. + * @param {Matrix} matrixB The second matrix multiplicand. + * @returns {Matrix} This matrix set to the product of matrixA x matrixB. + * @throws {ArgumentError} If either specified matrix is null or undefined. + */ +Matrix.prototype.setToMultiply = function (matrixA, matrixB) { + if (!matrixA || !matrixB) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "setToMultiply", "missingMatrix")); + } + + var ma = matrixA, + mb = matrixB; + + this[0] = ma[0] * mb[0] + ma[1] * mb[4] + ma[2] * mb[8] + ma[3] * mb[12]; + this[1] = ma[0] * mb[1] + ma[1] * mb[5] + ma[2] * mb[9] + ma[3] * mb[13]; + this[2] = ma[0] * mb[2] + ma[1] * mb[6] + ma[2] * mb[10] + ma[3] * mb[14]; + this[3] = ma[0] * mb[3] + ma[1] * mb[7] + ma[2] * mb[11] + ma[3] * mb[15]; + + this[4] = ma[4] * mb[0] + ma[5] * mb[4] + ma[6] * mb[8] + ma[7] * mb[12]; + this[5] = ma[4] * mb[1] + ma[5] * mb[5] + ma[6] * mb[9] + ma[7] * mb[13]; + this[6] = ma[4] * mb[2] + ma[5] * mb[6] + ma[6] * mb[10] + ma[7] * mb[14]; + this[7] = ma[4] * mb[3] + ma[5] * mb[7] + ma[6] * mb[11] + ma[7] * mb[15]; + + this[8] = ma[8] * mb[0] + ma[9] * mb[4] + ma[10] * mb[8] + ma[11] * mb[12]; + this[9] = ma[8] * mb[1] + ma[9] * mb[5] + ma[10] * mb[9] + ma[11] * mb[13]; + this[10] = ma[8] * mb[2] + ma[9] * mb[6] + ma[10] * mb[10] + ma[11] * mb[14]; + this[11] = ma[8] * mb[3] + ma[9] * mb[7] + ma[10] * mb[11] + ma[11] * mb[15]; + + this[12] = ma[12] * mb[0] + ma[13] * mb[4] + ma[14] * mb[8] + ma[15] * mb[12]; + this[13] = ma[12] * mb[1] + ma[13] * mb[5] + ma[14] * mb[9] + ma[15] * mb[13]; + this[14] = ma[12] * mb[2] + ma[13] * mb[6] + ma[14] * mb[10] + ma[15] * mb[14]; + this[15] = ma[12] * mb[3] + ma[13] * mb[7] + ma[14] * mb[11] + ma[15] * mb[15]; + + return this; +}; + +/** + * Sets this matrix to the symmetric covariance Matrix computed from the x, y, z coordinates of a specified + * points array. + *
+ * The computed covariance matrix represents the correlation between each pair of x-, y-, and z-coordinates as + * they're distributed about the point array's arithmetic mean. Its layout is as follows: + * + * C(x, x) C(x, y) C(x, z)
C(x, y) C(y, y) C(y, z)
C(x, z) C(y, z) C(z, z)
+ *
+ * C(i, j) is the covariance of coordinates i and j, where i or j are a coordinate's dispersion about its mean
+ * value. If any entry is zero, then there's no correlation between the two coordinates defining that entry. If the
+ * returned matrix is diagonal, then all three coordinates are uncorrelated, and the specified point is
+ * distributed evenly about its mean point.
+ * @param {Float32Array | Float64Array | Number[]} points The points to consider.
+ * @returns {Matrix} This matrix set to the covariance matrix for the specified list of points.
+ * @throws {ArgumentError} If the specified array of points is null, undefined or empty.
+ */
+Matrix.prototype.setToCovarianceOfPoints = function (points) {
+ if (!points || points.length < 1) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "setToCovarianceOfPoints", "missingArray"));
+ }
+
+ var mean,
+ dx,
+ dy,
+ dz,
+ count = 0,
+ c11 = 0,
+ c22 = 0,
+ c33 = 0,
+ c12 = 0,
+ c13 = 0,
+ c23 = 0,
+ vec = new Vec3(0, 0, 0);
+
+ mean = Vec3.averageOfBuffer(points, new Vec3(0, 0, 0));
+
+ for (var i = 0, len = points.length / 3; i < len; i++) {
+ vec[0] = points[i * 3];
+ vec[1] = points[i * 3 + 1];
+ vec[2] = points[i * 3 + 2];
+
+ dx = vec[0] - mean[0];
+ dy = vec[1] - mean[1];
+ dz = vec[2] - mean[2];
+
+ ++count;
+ c11 += dx * dx;
+ c22 += dy * dy;
+ c33 += dz * dz;
+ c12 += dx * dy; // c12 = c21
+ c13 += dx * dz; // c13 = c31
+ c23 += dy * dz; // c23 = c32
+ }
+
+ // Row 1
+ this[0] = c11 / count;
+ this[1] = c12 / count;
+ this[2] = c13 / count;
+ this[3] = 0;
+
+ // Row 2
+ this[4] = c12 / count;
+ this[5] = c22 / count;
+ this[6] = c23 / count;
+ this[7] = 0;
+
+ // Row 3
+ this[8] = c13 / count;
+ this[9] = c23 / count;
+ this[10] = c33 / count;
+ this[11] = 0;
+
+ // Row 4
+ this[12] = 0;
+ this[13] = 0;
+ this[14] = 0;
+ this[15] = 0;
+
+ return this;
+};
+
+/**
+ * Multiplies this matrix by a translation matrix with specified translation values.
+ * @param {Number} x The X translation component.
+ * @param {Number} y The Y translation component.
+ * @param {Number} z The Z translation component.
+ * @returns {Matrix} This matrix multiplied by the translation matrix implied by the specified values.
+ */
+Matrix.prototype.multiplyByTranslation = function (x, y, z) {
+
+ this.multiply(
+ 1, 0, 0, x,
+ 0, 1, 0, y,
+ 0, 0, 1, z,
+ 0, 0, 0, 1);
+
+ return this;
+};
+
+/**
+ * Multiplies this matrix by a rotation matrix about a specified axis and angle.
+ * @param {Number} x The X component of the rotation axis.
+ * @param {Number} y The Y component of the rotation axis.
+ * @param {Number} z The Z component of the rotation axis.
+ * @param {Number} angleDegrees The angle to rotate, in degrees.
+ * @returns {Matrix} This matrix multiplied by the rotation matrix implied by the specified values.
+ */
+Matrix.prototype.multiplyByRotation = function (x, y, z, angleDegrees) {
+
+ var c = Math.cos(angleDegrees * Angle.DEGREES_TO_RADIANS),
+ s = Math.sin(angleDegrees * Angle.DEGREES_TO_RADIANS);
+
+ this.multiply(
+ c + (1 - c) * x * x, (1 - c) * x * y - s * z, (1 - c) * x * z + s * y, 0,
+ (1 - c) * x * y + s * z, c + (1 - c) * y * y, (1 - c) * y * z - s * x, 0,
+ (1 - c) * x * z - s * y, (1 - c) * y * z + s * x, c + (1 - c) * z * z, 0,
+ 0, 0, 0, 1);
+
+ return this;
+};
+
+/**
+ * Multiplies this matrix by a scale matrix with specified values.
+ * @param {Number} xScale The X scale component.
+ * @param {Number} yScale The Y scale component.
+ * @param {Number} zScale The Z scale component.
+ * @returns {Matrix} This matrix multiplied by the scale matrix implied by the specified values.
+ */
+Matrix.prototype.multiplyByScale = function (xScale, yScale, zScale) {
+
+ this.multiply(
+ xScale, 0, 0, 0,
+ 0, yScale, 0, 0,
+ 0, 0, zScale, 0,
+ 0, 0, 0, 1);
+
+ return this;
+};
+
+/**
+ * Sets this matrix to one that flips and shifts the y-axis.
+ * + * The resultant matrix maps Y=0 to Y=1 and Y=1 to Y=0. All existing values are overwritten. This matrix is + * usually used to change the coordinate origin from an upper left coordinate origin to a lower left coordinate + * origin. This is typically necessary to align the coordinate system of images (top-left origin) with that of + * OpenGL (bottom-left origin). + * @returns {Matrix} This matrix set to values described above. + */ +Matrix.prototype.setToUnitYFlip = function () { + + this[0] = 1; + this[1] = 0; + this[2] = 0; + this[3] = 0; + this[4] = 0; + this[5] = -1; + this[6] = 0; + this[7] = 1; + this[8] = 0; + this[9] = 0; + this[10] = 1; + this[11] = 0; + this[12] = 0; + this[13] = 0; + this[14] = 0; + this[15] = 1; + + return this; +}; + +/** + * Multiplies this matrix by a local coordinate system transform for the specified globe. + *
+ * The local coordinate system is defined such that the local origin (0, 0, 0) maps to the specified origin + * point, the z axis maps to the globe's surface normal at the point, the y-axis maps to the north pointing + * tangent, and the x-axis maps to the east pointing tangent. + * + * @param {Vec3} origin The local coordinate system origin, in model coordinates. + * @param {Globe} globe The globe the coordinate system is relative to. + * + * @throws {ArgumentError} If either argument is null or undefined. + */ +Matrix.prototype.multiplyByLocalCoordinateTransform = function (origin, globe) { + if (!origin) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "multiplyByLocalCoordinateTransform", + "Origin vector is null or undefined")); + } + + if (!globe) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "multiplyByLocalCoordinateTransform", + "missingGlobe")); + } + + var xAxis = new Vec3(0, 0, 0), + yAxis = new Vec3(0, 0, 0), + zAxis = new Vec3(0, 0, 0); + + WWMath.localCoordinateAxesAtPoint(origin, globe, xAxis, yAxis, zAxis); + + this.multiply( + xAxis[0], yAxis[0], zAxis[0], origin[0], + xAxis[1], yAxis[1], zAxis[1], origin[1], + xAxis[2], yAxis[2], zAxis[2], origin[2], + 0, 0, 0, 1); + + return this; +}; + +/** + * Multiplies this matrix by a texture transform for the specified texture. + *
+ * A texture image transform maps the bottom-left corner of the texture's image data to coordinate [0,0] and maps the + * top-right of the texture's image data to coordinate [1,1]. This correctly handles textures whose image data has + * non-power-of-two dimensions, and correctly orients textures whose image data has its origin in the upper-left corner. + * + * @param {Texture} texture The texture to multiply a transform for. + * + * @throws {ArgumentError} If the texture is null or undefined. + */ +Matrix.prototype.multiplyByTextureTransform = function (texture) { + if (!texture) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "multiplyByTextureTransform", + "missingTexture")); + } + + // Compute the scale necessary to map the edge of the image data to the range [0,1]. When the texture contains + // power-of-two image data the scale is 1 and has no effect. Otherwise, the scale is computed such that the portion + // of the texture containing image data maps to the range [0,1]. + var sx = texture.originalImageWidth / texture.imageWidth, + sy = texture.originalImageHeight / texture.imageHeight; + + // Multiply this by a scaling matrix that maps the texture's image data to the range [0,1] and inverts the y axis. + // We have precomputed the result here in order to avoid an unnecessary matrix multiplication. + this.multiply( + sx, 0, 0, 0, + 0, -sy, 0, sy, + 0, 0, 1, 0, + 0, 0, 0, 1); + + return this; +}; + +/** + * Returns the translation components of this matrix. + * @param {Vec3} result A pre-allocated {@link Vec3} in which to return the translation components. + * @returns {Vec3} The specified result argument set to the translation components of this matrix. + * @throws {ArgumentError} If the specified result argument is null or undefined. + */ +Matrix.prototype.extractTranslation = function (result) { + if (!result) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "extractTranslation", "missingResult")); + } + + result[0] = this[3]; + result[1] = this[7]; + result[2] = this[11]; + + return result; +}; + +/** + * Returns the rotation angles of this matrix. + * @param {Vec3} result A pre-allocated {@link Vec3} in which to return the rotation angles. + * @returns {Vec3} The specified result argument set to the rotation angles of this matrix. The angles are in + * degrees. + * @throws {ArgumentError} If the specified result argument is null or undefined. + */ +Matrix.prototype.extractRotationAngles = function (result) { + if (!result) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "extractRotationAngles", "missingResult")); + } + + // Taken from Extracting Euler Angles from a Rotation Matrix by Mike Day, Insomniac Games. + // http://www.insomniacgames.com/mike-day-extracting-euler-angles-from-a-rotation-matrix/ + + var x = Math.atan2(this[6], this[10]), + y = Math.atan2(-this[2], Math.sqrt(this[0] * this[0] + this[1] * this[1])), + cx = Math.cos(x), + sx = Math.sin(x), + z = Math.atan2(sx * this[8] - cx * this[4], cx * this[5] - sx * this[9]); + + result[0] = x * Angle.RADIANS_TO_DEGREES; + result[1] = y * Angle.RADIANS_TO_DEGREES; + result[2] = z * Angle.RADIANS_TO_DEGREES; + + return result; +}; + +/** + * Multiplies this matrix by a first person viewing matrix for the specified globe. + *
+ * A first person viewing matrix places the viewer's eye at the specified eyePosition. By default the viewer is looking + * straight down at the globe's surface from the eye position, with the globe's normal vector coming out of the screen + * and north pointing toward the top of the screen. + *
+ * Heading specifies the viewer's azimuth, or its angle relative to North. Heading values range from -180 degrees to 180 + * degrees. A heading of 0 degrees looks North, 90 degrees looks East, +-180 degrees looks South, and -90 degrees looks + * West. + *
+ * Tilt specifies the viewer's angle relative to the surface. Tilt values range from -180 degrees to 180 degrees. A tilt + * of 0 degrees looks straight down at the globe's surface, 90 degrees looks at the horizon, and 180 degrees looks + * straight up. Tilt values greater than 180 degrees cause the viewer to turn upside down, and are therefore rarely used. + *
+ * Roll specifies the viewer's angle relative to the horizon. Roll values range from -180 degrees to 180 degrees. A roll + * of 0 degrees orients the viewer so that up is pointing to the top of the screen, at 90 degrees up is pointing to the + * right, at +-180 degrees up is pointing to the bottom, and at -90 up is pointing to the left. + * + * @param {Position} eyePosition The viewer's geographic eye position relative to the specified globe. + * @param {Number} heading The viewer's angle relative to north, in degrees. + * @param {Number} tilt The viewer's angle relative to the surface, in degrees. + * @param {Number} roll The viewer's angle relative to the horizon, in degrees. + * @param {Globe} globe The globe the viewer is looking at. + * + * @throws {ArgumentError} If the specified position or globe is null or undefined. + */ +Matrix.prototype.multiplyByFirstPersonModelview = function (eyePosition, heading, tilt, roll, globe) { + if (!eyePosition) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "multiplyByFirstPersonModelview", "missingPosition")); + } + + if (!globe) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "multiplyByFirstPersonModelview", "missingGlobe")); + } + + var c, + s, + ex, ey, ez, + xx, xy, xz, + yx, yy, yz, + zx, zy, zz, + eyePoint = new Vec3(0, 0, 0), + xAxis = new Vec3(0, 0, 0), + yAxis = new Vec3(0, 0, 0), + zAxis = new Vec3(0, 0, 0); + + // Roll. Rotate the eye point in a counter-clockwise direction about the z axis. Note that we invert the sines used + // in the rotation matrix in order to produce the counter-clockwise rotation. We invert only the cosines since + // sin(-a) = -sin(a) and cos(-a) = cos(a). + c = Math.cos(roll * Angle.DEGREES_TO_RADIANS); + s = Math.sin(roll * Angle.DEGREES_TO_RADIANS); + this.multiply( + c, s, 0, 0, + -s, c, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1); + + // Tilt. Rotate the eye point in a counter-clockwise direction about the x axis. Note that we invert the sines used + // in the rotation matrix in order to produce the counter-clockwise rotation. We invert only the cosines since + // sin(-a) = -sin(a) and cos(-a) = cos(a). + c = Math.cos(tilt * Angle.DEGREES_TO_RADIANS); + s = Math.sin(tilt * Angle.DEGREES_TO_RADIANS); + this.multiply(1, 0, 0, 0, + 0, c, s, 0, + 0, -s, c, 0, + 0, 0, 0, 1); + + // Heading. Rotate the eye point in a clockwise direction about the z axis again. This has a different effect than + // roll when tilt is non-zero because the viewer is no longer looking down the z axis. + c = Math.cos(heading * Angle.DEGREES_TO_RADIANS); + s = Math.sin(heading * Angle.DEGREES_TO_RADIANS); + this.multiply(c, -s, 0, 0, + s, c, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1); + + // Compute the eye point in model coordinates. This point is mapped to the origin in the look at transform below. + globe.computePointFromPosition(eyePosition.latitude, eyePosition.longitude, eyePosition.altitude, eyePoint); + ex = eyePoint[0]; + ey = eyePoint[1]; + ez = eyePoint[2]; + + // Transform the origin to the local coordinate system at the eye point. + WWMath.localCoordinateAxesAtPoint(eyePoint, globe, xAxis, yAxis, zAxis); + xx = xAxis[0]; + xy = xAxis[1]; + xz = xAxis[2]; + yx = yAxis[0]; + yy = yAxis[1]; + yz = yAxis[2]; + zx = zAxis[0]; + zy = zAxis[1]; + zz = zAxis[2]; + + this.multiply(xx, xy, xz, -xx * ex - xy * ey - xz * ez, + yx, yy, yz, -yx * ex - yy * ey - yz * ez, + zx, zy, zz, -zx * ex - zy * ey - zz * ez, + 0, 0, 0, 1); + + return this; +}; + +/** + * Multiplies this matrix by a look at viewing matrix for the specified globe. + *
+ * A look at viewing matrix places the center of the screen at the specified lookAtPosition. By default the viewer is + * looking straight down at the look at position from the specified range, with the globe's normal vector coming out of + * the screen and north pointing toward the top of the screen. + *
+ * Range specifies the distance between the look at position and the viewer's eye point. Range values may be any positive + * real number. A range of 0 places the eye point at the look at point, while a positive range moves the eye point away + * from but still looking at the look at point. + *
+ * Heading specifies the viewer's azimuth, or its angle relative to North. Heading values range from -180 degrees to 180 + * degrees. A heading of 0 degrees looks North, 90 degrees looks East, +-180 degrees looks South, and -90 degrees looks + * West. + *
+ * Tilt specifies the viewer's angle relative to the surface. Tilt values range from -180 degrees to 180 degrees. A tilt + * of 0 degrees looks straight down at the globe's surface, 90 degrees looks at the horizon, and 180 degrees looks + * straight up. Tilt values greater than 180 degrees cause the viewer to turn upside down, and are therefore rarely used. + *
+ * Roll specifies the viewer's angle relative to the horizon. Roll values range from -180 degrees to 180 degrees. A roll + * of 0 degrees orients the viewer so that up is pointing to the top of the screen, at 90 degrees up is pointing to the + * right, at +-180 degrees up is pointing to the bottom, and at -90 up is pointing to the left. + * + * @param {Position} lookAtPosition The viewer's geographic look at position relative to the specified globe. + * @param {Number} range The distance between the eye point and the look at point, in model coordinates. + * @param {Number} heading The viewer's angle relative to north, in degrees. + * @param {Number} tilt The viewer's angle relative to the surface, in degrees. + * @param {Number} roll The viewer's angle relative to the horizon, in degrees. + * @param {Globe} globe The globe the viewer is looking at. + * + * @throws {ArgumentError} If either the specified look-at position or globe is null or undefined, or the + * specified range is less than zero. + */ +Matrix.prototype.multiplyByLookAtModelview = function (lookAtPosition, range, heading, tilt, roll, globe) { + if (!lookAtPosition) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "multiplyByLookAtModelview", "missingPosition")); + } + + if (range < 0) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "multiplyByLookAtModelview", + "Range is less than zero")); + } + + if (!globe) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "multiplyByLookAtModelview", "missingGlobe")); + } + + // Translate the eye point along the positive z axis while keeping the look at point in the center of the viewport. + this.multiplyByTranslation(0, 0, -range); + + // Transform the origin to the local coordinate system at the look at position, and rotate the viewer by the + // specified heading, tilt and roll. + this.multiplyByFirstPersonModelview(lookAtPosition, heading, tilt, roll, globe); + + return this; +}; + +/** + * Sets this matrix to a perspective projection matrix for the specified viewport dimensions and clip distances. + *
+ * A perspective projection matrix maps points in eye coordinates into clip coordinates in a way that causes + * distant objects to appear smaller, and preserves the appropriate depth information for each point. In model + * coordinates, a perspective projection is defined by frustum originating at the eye position and extending + * outward in the viewer's direction. The near distance and the far distance identify the minimum and maximum + * distance, respectively, at which an object in the scene is visible. Near and far distances must be positive + * and may not be equal. + * + * @param {Number} viewportWidth The viewport width, in screen coordinates. + * @param {Number} viewportHeight The viewport height, in screen coordinates. + * @param {Number} nearDistance The near clip plane distance, in model coordinates. + * @param {Number} farDistance The far clip plane distance, in model coordinates. + * @throws {ArgumentError} If the specified width or height is less than or equal to zero, if the near and far + * distances are equal, or if either the near or far distance are less than or equal to zero. + */ +Matrix.prototype.setToPerspectiveProjection = function (viewportWidth, viewportHeight, nearDistance, farDistance) { + if (viewportWidth <= 0) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "setToPerspectiveProjection", + "invalidWidth")); + } + + if (viewportHeight <= 0) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "setToPerspectiveProjection", + "invalidHeight")); + } + + if (nearDistance === farDistance) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "setToPerspectiveProjection", + "Near and far distance are the same.")); + } + + if (nearDistance <= 0 || farDistance <= 0) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "setToPerspectiveProjection", + "Near or far distance is less than or equal to zero.")); + } + + // Compute the dimensions of the viewport rectangle at the near distance. + var nearRect = WWMath.perspectiveFrustumRectangle(viewportWidth, viewportHeight, nearDistance), + left = nearRect.getMinX(), + right = nearRect.getMaxX(), + bottom = nearRect.getMinY(), + top = nearRect.getMaxY(); + + // Taken from Mathematics for 3D Game Programming and Computer Graphics, Second Edition, equation 4.52. + + // Row 1 + this[0] = 2 * nearDistance / (right - left); + this[1] = 0; + this[2] = (right + left) / (right - left); + this[3] = 0; + // Row 2 + this[4] = 0; + this[5] = 2 * nearDistance / (top - bottom); + this[6] = (top + bottom) / (top - bottom); + this[7] = 0; + // Row 3 + this[8] = 0; + this[9] = 0; + this[10] = -(farDistance + nearDistance) / (farDistance - nearDistance); + this[11] = -2 * nearDistance * farDistance / (farDistance - nearDistance); + // Row 4 + this[12] = 0; + this[13] = 0; + this[14] = -1; + this[15] = 0; + + return this; +}; + +/** + * Sets this matrix to a screen projection matrix for the specified viewport dimensions. + *
+ * A screen projection matrix is an orthographic projection that assumes that points in model coordinates + * represent a screen point and a depth. Screen projection matrices therefore map model coordinates directly + * into screen coordinates without modification. A point's xy coordinates are interpreted as literal screen + * coordinates and must be in the viewport to be visible. A point's z coordinate is interpreted as a depth value + * that ranges from 0 to 1. Additionally, the screen projection matrix preserves the depth value returned by + * [DrawContext.project]{@link DrawContext#project}. + * + * @param {Number} viewportWidth The viewport width, in screen coordinates. + * @param {Number} viewportHeight The viewport height, in screen coordinates. + * @throws {ArgumentError} If the specified width or height is less than or equal to zero. + */ +Matrix.prototype.setToScreenProjection = function (viewportWidth, viewportHeight) { + if (viewportWidth <= 0) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "setToScreenProjection", + "invalidWidth")); + } + + if (viewportHeight <= 0) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "setToScreenProjection", + "invalidHeight")); + } + + // Taken from Mathematics for 3D Game Programming and Computer Graphics, Second Edition, equation 4.57. + // Simplified to assume that the viewport origin is (0, 0). + // + // The third row of this projection matrix is configured so that points with z coordinates representing + // depth values ranging from 0 to 1 are not modified after transformation into window coordinates. This + // projection matrix maps z values in the range [0, 1] to the range [-1, 1] by applying the following + // function to incoming z coordinates: + // + // zp = z0 * 2 - 1 + // + // Where 'z0' is the point's z coordinate and 'zp' is the projected z coordinate. The GPU then maps the + // projected z coordinate into window coordinates in the range [0, 1] by applying the following function: + // + // zw = zp * 0.5 + 0.5 + // + // The result is that a point's z coordinate is effectively passed to the GPU without modification. + + // Row 1 + this[0] = 2 / viewportWidth; + this[1] = 0; + this[2] = 0; + this[3] = -1; + // Row 2 + this[4] = 0; + this[5] = 2 / viewportHeight; + this[6] = 0; + this[7] = -1; + // Row 3 + this[8] = 0; + this[9] = 0; + this[10] = 2; + this[11] = -1; + // Row 4 + this[12] = 0; + this[13] = 0; + this[14] = 0; + this[15] = 1; + + return this; +}; + +/** + * Returns this viewing matrix's eye point. + *
+ * This method assumes that this matrix represents a viewing matrix. If this does not represent a viewing matrix the + * results are undefined. + *
+ * In model coordinates, a viewing matrix's eye point is the point the viewer is looking from and maps to the center of + * the screen. + * + * @param {Vec3} result A pre-allocated {@link Vec3} in which to return the extracted values. + * @return {Vec3} The specified result argument containing the viewing matrix's eye point, in model coordinates. + * @throws {ArgumentError} If the specified result argument is null or undefined. + */ +Matrix.prototype.extractEyePoint = function (result) { + if (!result) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "extractEyePoint", "missingResult")); + } + + // The eye point of a modelview matrix is computed by transforming the origin (0, 0, 0, 1) by the matrix's inverse. + // This is equivalent to transforming the inverse of this matrix's translation components in the rightmost column by + // the transpose of its upper 3x3 components. + result[0] = -(this[0] * this[3]) - this[4] * this[7] - this[8] * this[11]; + result[1] = -(this[1] * this[3]) - this[5] * this[7] - this[9] * this[11]; + result[2] = -(this[2] * this[3]) - this[6] * this[7] - this[10] * this[11]; + + return result; +}; + +/** + * Returns this viewing matrix's forward vector. + *
+ * This method assumes that this matrix represents a viewing matrix. If this does not represent a viewing matrix the + * results are undefined. + * + * @param {Vec3} result A pre-allocated {@link Vec3} in which to return the extracted values. + * @return {Vec3} The specified result argument containing the viewing matrix's forward vector, in model coordinates. + * @throws {ArgumentError} If the specified result argument is null or undefined. + */ +Matrix.prototype.extractForwardVector = function (result) { + if (!result) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "extractForwardVector", "missingResult")); + } + + // The forward vector of a modelview matrix is computed by transforming the negative Z axis (0, 0, -1, 0) by the + // matrix's inverse. We have pre-computed the result inline here to simplify this computation. + result[0] = -this[8]; + result[1] = -this[9]; + result[2] = -this[10]; + + return result; +}; + +/** + * Extracts this viewing matrix's parameters given a viewing origin and a globe. + *
+ * This method assumes that this matrix represents a viewing matrix. If this does not represent a viewing matrix the + * results are undefined. + *
+ * This returns a parameterization of this viewing matrix based on the specified origin and globe. The origin indicates + * the model coordinate point that the view's orientation is relative to, while the globe provides the necessary model + * coordinate context for the origin and the orientation. The origin should be either the view's eye point or a point on + * the view's forward vector. The view's roll must be specified in order to disambiguate heading and roll when the view's + * tilt is zero. + *
+ * The following list outlines the returned key-value pairs and their meanings: + *
+ * This method assumes that this matrix represents a projection matrix. If this does not represent a projection + * matrix the results are undefined. Projection matrices can be created by calling + * [setToPerspectiveProjection]{@link Matrix#setToPerspectiveProjection} or [setToScreenProjection]{@link Matrix#setToScreenProjection}. + *
+ * The depth offset may be any real number and is typically used to draw geometry slightly closer to the user's + * eye in order to give those shapes visual priority over nearby or geometry. An offset of zero has no effect. + * An offset less than zero brings depth values closer to the eye, while an offset greater than zero pushes + * depth values away from the eye. + *
+ * Depth offset may be applied to both perspective and orthographic projection matrices. The effect on each + * projection type is outlined here: + *
+ * Perspective Projection + *
+ * The effect of depth offset on a perspective projection increases exponentially with distance from the eye. + * This has the effect of adjusting the offset for the loss in depth precision with geometry drawn further from + * the eye. Distant geometry requires a greater offset to differentiate itself from nearby geometry, while close + * geometry does not. + *
+ * Orthographic Projection + *
+ * The effect of depth offset on an orthographic projection increases linearly with distance from the eye. While + * it is reasonable to apply a depth offset to an orthographic projection, the effect is most appropriate when + * applied to the projection used to draw the scene. For example, when an object's coordinates are projected by + * a perspective projection into screen coordinates then drawn using an orthographic projection, it is best to + * apply the offset to the original perspective projection. The method [DrawContext.project]{@link DrawContext#project} performs the + * correct behavior for the projection type used to draw the scene. + * + * @param {Number} depthOffset The amount of offset to apply. + * @returns {Matrix} This matrix with it's depth offset set to the specified offset. + */ +Matrix.prototype.offsetProjectionDepth = function (depthOffset) { + + this[10] *= 1 + depthOffset; + + return this; +}; + +/** + * Multiplies this matrix by a specified matrix. + * + * @param {Matrix} matrix The matrix to multiply with this matrix. + * @returns {Matrix} This matrix after multiplying it by the specified matrix. + * @throws {ArgumentError} if the specified matrix is null or undefined. + */ +Matrix.prototype.multiplyMatrix = function (matrix) { + if (!matrix) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "multiplyMatrix", "missingMatrix")); + } + + var ma = this, + mb = matrix, + ma0, ma1, ma2, ma3; + + // Row 1 + ma0 = ma[0]; + ma1 = ma[1]; + ma2 = ma[2]; + ma3 = ma[3]; + ma[0] = ma0 * mb[0] + ma1 * mb[4] + ma2 * mb[8] + ma3 * mb[12]; + ma[1] = ma0 * mb[1] + ma1 * mb[5] + ma2 * mb[9] + ma3 * mb[13]; + ma[2] = ma0 * mb[2] + ma1 * mb[6] + ma2 * mb[10] + ma3 * mb[14]; + ma[3] = ma0 * mb[3] + ma1 * mb[7] + ma2 * mb[11] + ma3 * mb[15]; + + // Row 2 + ma0 = ma[4]; + ma1 = ma[5]; + ma2 = ma[6]; + ma3 = ma[7]; + ma[4] = ma0 * mb[0] + ma1 * mb[4] + ma2 * mb[8] + ma3 * mb[12]; + ma[5] = ma0 * mb[1] + ma1 * mb[5] + ma2 * mb[9] + ma3 * mb[13]; + ma[6] = ma0 * mb[2] + ma1 * mb[6] + ma2 * mb[10] + ma3 * mb[14]; + ma[7] = ma0 * mb[3] + ma1 * mb[7] + ma2 * mb[11] + ma3 * mb[15]; + + // Row 3 + ma0 = ma[8]; + ma1 = ma[9]; + ma2 = ma[10]; + ma3 = ma[11]; + ma[8] = ma0 * mb[0] + ma1 * mb[4] + ma2 * mb[8] + ma3 * mb[12]; + ma[9] = ma0 * mb[1] + ma1 * mb[5] + ma2 * mb[9] + ma3 * mb[13]; + ma[10] = ma0 * mb[2] + ma1 * mb[6] + ma2 * mb[10] + ma3 * mb[14]; + ma[11] = ma0 * mb[3] + ma1 * mb[7] + ma2 * mb[11] + ma3 * mb[15]; + + // Row 4 + ma0 = ma[12]; + ma1 = ma[13]; + ma2 = ma[14]; + ma3 = ma[15]; + ma[12] = ma0 * mb[0] + ma1 * mb[4] + ma2 * mb[8] + ma3 * mb[12]; + ma[13] = ma0 * mb[1] + ma1 * mb[5] + ma2 * mb[9] + ma3 * mb[13]; + ma[14] = ma0 * mb[2] + ma1 * mb[6] + ma2 * mb[10] + ma3 * mb[14]; + ma[15] = ma0 * mb[3] + ma1 * mb[7] + ma2 * mb[11] + ma3 * mb[15]; + + return this; +}; + +/** + * Multiplies this matrix by a matrix specified by individual components. + * + * @param {Number} m00 matrix element at row 1, column 1. + * @param {Number} m01 matrix element at row 1, column 2. + * @param {Number} m02 matrix element at row 1, column 3. + * @param {Number} m03 matrix element at row 1, column 4. + * @param {Number} m10 matrix element at row 2, column 1. + * @param {Number} m11 matrix element at row 2, column 2. + * @param {Number} m12 matrix element at row 2, column 3. + * @param {Number} m13 matrix element at row 2, column 4. + * @param {Number} m20 matrix element at row 3, column 1. + * @param {Number} m21 matrix element at row 3, column 2. + * @param {Number} m22 matrix element at row 3, column 3. + * @param {Number} m23 matrix element at row 3, column 4. + * @param {Number} m30 matrix element at row 4, column 1. + * @param {Number} m31 matrix element at row 4, column 2. + * @param {Number} m32 matrix element at row 4, column 3. + * @param {Number} m33 matrix element at row 4, column 4. + * @returns {Matrix} This matrix with its components multiplied by the specified values. + */ +Matrix.prototype.multiply = function (m00, m01, m02, m03, + m10, m11, m12, m13, + m20, m21, m22, m23, + m30, m31, m32, m33) { + + var ma = this, + ma0, ma1, ma2, ma3; + + // Row 1 + ma0 = ma[0]; + ma1 = ma[1]; + ma2 = ma[2]; + ma3 = ma[3]; + ma[0] = ma0 * m00 + ma1 * m10 + ma2 * m20 + ma3 * m30; + ma[1] = ma0 * m01 + ma1 * m11 + ma2 * m21 + ma3 * m31; + ma[2] = ma0 * m02 + ma1 * m12 + ma2 * m22 + ma3 * m32; + ma[3] = ma0 * m03 + ma1 * m13 + ma2 * m23 + ma3 * m33; + + // Row 2 + ma0 = ma[4]; + ma1 = ma[5]; + ma2 = ma[6]; + ma3 = ma[7]; + ma[4] = ma0 * m00 + ma1 * m10 + ma2 * m20 + ma3 * m30; + ma[5] = ma0 * m01 + ma1 * m11 + ma2 * m21 + ma3 * m31; + ma[6] = ma0 * m02 + ma1 * m12 + ma2 * m22 + ma3 * m32; + ma[7] = ma0 * m03 + ma1 * m13 + ma2 * m23 + ma3 * m33; + + // Row 3 + ma0 = ma[8]; + ma1 = ma[9]; + ma2 = ma[10]; + ma3 = ma[11]; + ma[8] = ma0 * m00 + ma1 * m10 + ma2 * m20 + ma3 * m30; + ma[9] = ma0 * m01 + ma1 * m11 + ma2 * m21 + ma3 * m31; + ma[10] = ma0 * m02 + ma1 * m12 + ma2 * m22 + ma3 * m32; + ma[11] = ma0 * m03 + ma1 * m13 + ma2 * m23 + ma3 * m33; + + // Row 4 + ma0 = ma[12]; + ma1 = ma[13]; + ma2 = ma[14]; + ma3 = ma[15]; + ma[12] = ma0 * m00 + ma1 * m10 + ma2 * m20 + ma3 * m30; + ma[13] = ma0 * m01 + ma1 * m11 + ma2 * m21 + ma3 * m31; + ma[14] = ma0 * m02 + ma1 * m12 + ma2 * m22 + ma3 * m32; + ma[15] = ma0 * m03 + ma1 * m13 + ma2 * m23 + ma3 * m33; + + return this; +}; + +/** + * Inverts the specified matrix and stores the result in this matrix. + *
+ * This throws an exception if the specified matrix is singular. + *
+ * The result of this method is undefined if this matrix is passed in as the matrix to invert. + * + * @param {Matrix} matrix The matrix whose inverse is computed. + * @returns {Matrix} This matrix set to the inverse of the specified matrix. + * + * @throws {ArgumentError} If the specified matrix is null, undefined or cannot be inverted. + */ +Matrix.prototype.invertMatrix = function (matrix) { + if (!matrix) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "invertMatrix", "missingMatrix")); + } + + // Copy the specified matrix into a mutable two-dimensional array. + var A = [[], [], [], []]; + A[0][0] = matrix[0]; + A[0][1] = matrix[1]; + A[0][2] = matrix[2]; + A[0][3] = matrix[3]; + A[1][0] = matrix[4]; + A[1][1] = matrix[5]; + A[1][2] = matrix[6]; + A[1][3] = matrix[7]; + A[2][0] = matrix[8]; + A[2][1] = matrix[9]; + A[2][2] = matrix[10]; + A[2][3] = matrix[11]; + A[3][0] = matrix[12]; + A[3][1] = matrix[13]; + A[3][2] = matrix[14]; + A[3][3] = matrix[15]; + + var index = [], + d = Matrix.ludcmp(A, index), + i, + j; + + // Compute the matrix's determinant. + for (i = 0; i < 4; i += 1) { + d *= A[i][i]; + } + + // The matrix is singular if its determinant is zero or very close to zero. + if (Math.abs(d) < 1.0e-8) + return null; + + var Y = [[], [], [], []], + col = []; + for (j = 0; j < 4; j += 1) { + for (i = 0; i < 4; i += 1) { + col[i] = 0.0; + } + + col[j] = 1.0; + Matrix.lubksb(A, index, col); + + for (i = 0; i < 4; i += 1) { + Y[i][j] = col[i]; + } + } + + this[0] = Y[0][0]; + this[1] = Y[0][1]; + this[2] = Y[0][2]; + this[3] = Y[0][3]; + this[4] = Y[1][0]; + this[5] = Y[1][1]; + this[6] = Y[1][2]; + this[7] = Y[1][3]; + this[8] = Y[2][0]; + this[9] = Y[2][1]; + this[10] = Y[2][2]; + this[11] = Y[2][3]; + this[12] = Y[3][0]; + this[13] = Y[3][1]; + this[14] = Y[3][2]; + this[15] = Y[3][3]; + + return this; +}; + +/* Internal. Intentionally not documented. + * Utility method to solve a linear system with an LU factorization of a matrix. + * Solves Ax=b, where A is in LU factorized form. + * Algorithm derived from "Numerical Recipes in C", Press et al., 1988. + * + * @param {Number[]} A An LU factorization of a matrix. + * @param {Number[]} index Permutation vector of that LU factorization. + * @param {Number[]} b Vector to be solved. + */ +// Method "lubksb" derived from "Numerical Recipes in C", Press et al., 1988 +Matrix.lubksb = function (A, index, b) { + var ii = -1, + i, + j, + sum; + for (i = 0; i < 4; i += 1) { + var ip = index[i]; + sum = b[ip]; + b[ip] = b[i]; + + if (ii != -1) { + for (j = ii; j <= i - 1; j += 1) { + sum -= A[i][j] * b[j]; + } + } + else if (sum != 0.0) { + ii = i; + } + + b[i] = sum; + } + + for (i = 3; i >= 0; i -= 1) { + sum = b[i]; + for (j = i + 1; j < 4; j += 1) { + sum -= A[i][j] * b[j]; + } + + b[i] = sum / A[i][i]; + } +}; + +/* Internal. Intentionally not documented. + * Utility method to perform an LU factorization of a matrix. + * "ludcmp" is derived from "Numerical Recipes in C", Press et al., 1988. + * + * @param {Number[]} A matrix to be factored + * @param {Number[]} index permutation vector + * @returns {Number} Condition number of matrix. + */ +Matrix.ludcmp = function (A, index) { + var TINY = 1.0e-20, + vv = [], /* new double[4]; */ + d = 1.0, + temp, + i, + j, + k, + big, + sum, + imax, + dum; + for (i = 0; i < 4; i += 1) { + big = 0.0; + for (j = 0; j < 4; j += 1) { + if ((temp = Math.abs(A[i][j])) > big) { + big = temp; + } + } + + if (big == 0.0) { + return 0.0; // Matrix is singular if the entire row contains zero. + } + else { + vv[i] = 1.0 / big; + } + } + + for (j = 0; j < 4; j += 1) { + for (i = 0; i < j; i += 1) { + sum = A[i][j]; + for (k = 0; k < i; k += 1) { + sum -= A[i][k] * A[k][j]; + } + + A[i][j] = sum; + } + + big = 0.0; + imax = -1; + for (i = j; i < 4; i += 1) { + sum = A[i][j]; + for (k = 0; k < j; k++) { + sum -= A[i][k] * A[k][j]; + } + + A[i][j] = sum; + + if ((dum = vv[i] * Math.abs(sum)) >= big) { + big = dum; + imax = i; + } + } + + if (j != imax) { + for (k = 0; k < 4; k += 1) { + dum = A[imax][k]; + A[imax][k] = A[j][k]; + A[j][k] = dum; + } + + d = -d; + vv[imax] = vv[j]; + } + + index[j] = imax; + if (A[j][j] == 0.0) + A[j][j] = TINY; + + if (j != 3) { + dum = 1.0 / A[j][j]; + for (i = j + 1; i < 4; i += 1) { + A[i][j] *= dum; + } + } + } + + return d; +}; + +/** + * Inverts the specified matrix and stores the result in this matrix. + *
+ * The specified matrix is assumed to represent an orthonormal transform matrix. This matrix's upper 3x3 is + * transposed, then its fourth column is transformed by the transposed upper 3x3 and negated. + *
+ * The result of this method is undefined if this matrix is passed in as the matrix to invert. + * + * @param {Matrix} matrix The matrix whose inverse is computed. This matrix is assumed to represent an + * orthonormal transform matrix. + * @returns {Matrix} This matrix set to the inverse of the specified matrix. + * + * @throws {ArgumentError} If the specified matrix is null or undefined. + */ +Matrix.prototype.invertOrthonormalMatrix = function (matrix) { + if (!matrix) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "invertOrthonormalMatrix", "missingMatrix")); + } + + // 'a' is assumed to contain a 3D transformation matrix. + // Upper-3x3 is inverted, translation is transformed by inverted-upper-3x3 and negated. + + var a = matrix; + + this[0] = a[0]; + this[1] = a[4]; + this[2] = a[8]; + this[3] = 0.0 - a[0] * a[3] - a[4] * a[7] - a[8] * a[11]; + + this[4] = a[1]; + this[5] = a[5]; + this[6] = a[9]; + this[7] = 0.0 - a[1] * a[3] - a[5] * a[7] - a[9] * a[11]; + + this[8] = a[2]; + this[9] = a[6]; + this[10] = a[10]; + this[11] = 0.0 - a[2] * a[3] - a[6] * a[7] - a[10] * a[11]; + + this[12] = 0; + this[13] = 0; + this[14] = 0; + this[15] = 1; + + return this; +}; + +/** + * Computes the eigenvectors of this matrix. + *
+ * The eigenvectors are returned sorted from the most prominent vector to the least prominent vector. + * Each eigenvector has length equal to its corresponding eigenvalue. + * + * @param {Vec3} result1 A pre-allocated vector in which to return the most prominent eigenvector. + * @param {Vec3} result2 A pre-allocated vector in which to return the second most prominent eigenvector. + * @param {Vec3} result3 A pre-allocated vector in which to return the least prominent eigenvector. + * + * @throws {ArgumentError} if any argument is null or undefined or if this matrix is not symmetric. + */ +Matrix.prototype.eigensystemFromSymmetricMatrix = function (result1, result2, result3) { + if (!result1 || !result2 || !result3) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "eigensystemFromSymmetricMatrix", "missingResult")); + } + + if (this[1] != this[4] || this[2] != this[8] || this[6] != this[9]) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "eigensystemFromSymmetricMatrix", + "Matrix is not symmetric")); + } + + // Taken from Mathematics for 3D Game Programming and Computer Graphics, Second Edition, listing 14.6. + + var epsilon = 1.0e-10, + // Since the matrix is symmetric m12=m21, m13=m31 and m23=m32, therefore we can ignore the values m21, + // m32 and m32. + m11 = this[0], + m12 = this[1], + m13 = this[2], + m22 = this[5], + m23 = this[6], + m33 = this[10], + r = [ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1] + ], + maxSweeps = 32, + u, u2, u2p1, t, c, s, temp, i, i1, i2, i3; + + for (var a = 0; a < maxSweeps; a++) { + // Exit if off-diagonal entries small enough + if (WWMath.fabs(m12) < epsilon && WWMath.fabs(m13) < epsilon && WWMath.fabs(m23) < epsilon) + break; + + // Annihilate (1,2) entry. + if (m12 != 0) { + u = (m22 - m11) * 0.5 / m12; + u2 = u * u; + u2p1 = u2 + 1; + t = u2p1 != u2 ? (u < 0 ? -1 : 1) * (Math.sqrt(u2p1) - WWMath.fabs(u)) : 0.5 / u; + c = 1 / Math.sqrt(t * t + 1); + s = c * t; + + m11 -= t * m12; + m22 += t * m12; + m12 = 0; + + temp = c * m13 - s * m23; + m23 = s * m13 + c * m23; + m13 = temp; + + for (i = 0; i < 3; i++) { + temp = c * r[i][0] - s * r[i][1]; + r[i][1] = s * r[i][0] + c * r[i][1]; + r[i][0] = temp; + } + } + + // Annihilate (1,3) entry. + if (m13 != 0) { + u = (m33 - m11) * 0.5 / m13; + u2 = u * u; + u2p1 = u2 + 1; + t = u2p1 != u2 ? (u < 0 ? -1 : 1) * (Math.sqrt(u2p1) - WWMath.fabs(u)) : 0.5 / u; + c = 1 / Math.sqrt(t * t + 1); + s = c * t; + + m11 -= t * m13; + m33 += t * m13; + m13 = 0; + + temp = c * m12 - s * m23; + m23 = s * m12 + c * m23; + m12 = temp; + + for (i = 0; i < 3; i++) { + temp = c * r[i][0] - s * r[i][2]; + r[i][2] = s * r[i][0] + c * r[i][2]; + r[i][0] = temp; + } + } + + // Annihilate (2,3) entry. + if (m23 != 0) { + u = (m33 - m22) * 0.5 / m23; + u2 = u * u; + u2p1 = u2 + 1; + t = u2p1 != u2 ? (u < 0 ? -1 : 1) * (Math.sqrt(u2p1) - WWMath.fabs(u)) : 0.5 / u; + c = 1 / Math.sqrt(t * t + 1); + s = c * t; + + m22 -= t * m23; + m33 += t * m23; + m23 = 0; + + temp = c * m12 - s * m13; + m13 = s * m12 + c * m13; + m12 = temp; + + for (i = 0; i < 3; i++) { + temp = c * r[i][1] - s * r[i][2]; + r[i][2] = s * r[i][1] + c * r[i][2]; + r[i][1] = temp; + } + } + } + + i1 = 0; + i2 = 1; + i3 = 2; + + if (m11 < m22) { + temp = m11; + m11 = m22; + m22 = temp; + + temp = i1; + i1 = i2; + i2 = temp; + } + + if (m22 < m33) { + temp = m22; + m22 = m33; + m33 = temp; + + temp = i2; + i2 = i3; + i3 = temp; + } + + if (m11 < m22) { + temp = m11; + m11 = m22; + m22 = temp; + + temp = i1; + i1 = i2; + i2 = temp; + } + + result1[0] = r[0][i1]; + result1[1] = r[1][i1]; + result1[2] = r[2][i1]; + + result2[0] = r[0][i2]; + result2[1] = r[1][i2]; + result2[2] = r[2][i2]; + + result3[0] = r[0][i3]; + result3[1] = r[1][i3]; + result3[2] = r[2][i3]; + + result1.normalize(); + result2.normalize(); + result3.normalize(); + + result1.multiply(m11); + result2.multiply(m22); + result3.multiply(m33); +}; + +/** + * Extracts and returns a new matrix whose upper 3x3 entries are identical to those of this matrix, + * and whose fourth row and column are 0 except for a 1 in the diagonal position. + * @returns {Matrix} The upper 3x3 matrix of this matrix. + */ +Matrix.prototype.upper3By3 = function () { + var result = Matrix.fromIdentity(); + + result[0] = this[0]; + result[1] = this[1]; + result[2] = this[2]; + + result[4] = this[4]; + result[5] = this[5]; + result[6] = this[6]; + + result[8] = this[8]; + result[9] = this[9]; + result[10] = this[10]; + + return result; +}; + + +/** + * Transforms the specified screen point from WebGL screen coordinates to model coordinates. This method assumes + * this matrix represents an inverse modelview-projection matrix. The result of this method is + * undefined if this matrix is not an inverse modelview-projection matrix. + *
+ * The screen point is understood to be in WebGL screen coordinates, with the origin in the bottom-left corner + * and axes that extend up and to the right from the origin. + *
+ * This function stores the transformed point in the result argument, and returns true or false to indicate whether the + * transformation is successful. It returns false if the modelview or projection matrices + * are malformed, or if the screenPoint is clipped by the near clipping plane or the far clipping plane. + * + * @param {Vec3} screenPoint The screen coordinate point to un-project. + * @param {Rectangle} viewport The viewport defining the screen point's coordinate system + * @param {Vec3} result A pre-allocated vector in which to return the unprojected point. + * @returns {boolean} true if the transformation is successful, otherwise false. + * @throws {ArgumentError} If either the specified point or result argument is null or undefined. + */ +Matrix.prototype.unProject = function (screenPoint, viewport, result) { + if (!screenPoint) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "unProject", + "missingPoint")); + } + + if (!viewport) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "unProject", + "missingViewport")); + } + + if (!result) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "unProject", + "missingResult")); + } + + var sx = screenPoint[0], + sy = screenPoint[1], + sz = screenPoint[2]; + + // Convert the XY screen coordinates to coordinates in the range [0, 1]. This enables the XY coordinates to + // be converted to clip coordinates. + sx = (sx - viewport.x) / viewport.width; + sy = (sy - viewport.y) / viewport.height; + + // Convert from coordinates in the range [0, 1] to clip coordinates in the range [-1, 1]. + sx = sx * 2 - 1; + sy = sy * 2 - 1; + sz = sz * 2 - 1; + + // Clip the point against the near and far clip planes. In clip coordinates the near and far clip planes are + // perpendicular to the Z axis and are located at -1 and 1, respectively. + if (sz < -1 || sz > 1) { + return false; + } + + // Transform the screen point from clip coordinates to model coordinates. This inverts the Z axis and stores + // the negative of the eye coordinate Z value in the W coordinate. + var + x = this[0] * sx + this[1] * sy + this[2] * sz + this[3], + y = this[4] * sx + this[5] * sy + this[6] * sz + this[7], + z = this[8] * sx + this[9] * sy + this[10] * sz + this[11], + w = this[12] * sx + this[13] * sy + this[14] * sz + this[15]; + + if (w === 0) { + return false; + } + + // Complete the conversion from model coordinates to clip coordinates by dividing by W. + result[0] = x / w; + result[1] = y / w; + result[2] = z / w; + + return true; +}; + +export default Matrix; + + diff --git a/web/test/WebWorldWind/src/geom/Matrix3.js b/web/test/WebWorldWind/src/geom/Matrix3.js new file mode 100644 index 00000000..8d23bf96 --- /dev/null +++ b/web/test/WebWorldWind/src/geom/Matrix3.js @@ -0,0 +1,228 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports Matrix3 + */ + +import ArgumentError from '../error/ArgumentError'; +import Logger from '../util/Logger'; + + +/** + * Constructs a 3 x 3 matrix. + * @alias Matrix3 + * @constructor + * @classdesc Represents a 3 x 3 double precision matrix stored in a Float64Array in row-major order. + * @param {Number} m11 matrix element at row 1, column 1. + * @param {Number} m12 matrix element at row 1, column 2. + * @param {Number} m13 matrix element at row 1, column 3. + * @param {Number} m21 matrix element at row 2, column 1. + * @param {Number} m22 matrix element at row 2, column 2. + * @param {Number} m23 matrix element at row 2, column 3. + * @param {Number} m31 matrix element at row 3, column 1. + * @param {Number} m32 matrix element at row 3, column 2. + * @param {Number} m33 matrix element at row 3, column 3. + */ +function Matrix3(m11, m12, m13, + m21, m22, m23, + m31, m32, m33) { + this[0] = m11; + this[1] = m12; + this[2] = m13; + this[3] = m21; + this[4] = m22; + this[5] = m23; + this[6] = m31; + this[7] = m32; + this[8] = m33; +} + +// Derives from Float64Array. +Matrix3.prototype = new Float64Array(9); + +/** + * Creates an identity matrix. + * @returns {Matrix3} A new identity matrix. + */ +Matrix3.fromIdentity = function () { + return new Matrix3( + 1, 0, 0, + 0, 1, 0, + 0, 0, 1 + ); +}; + +/** + * Sets this matrix to one that flips and shifts the y-axis. + *
+ * The resultant matrix maps Y=0 to Y=1 and Y=1 to Y=0. All existing values are overwritten. This matrix is + * usually used to change the coordinate origin from an upper left coordinate origin to a lower left coordinate + * origin. This is typically necessary to align the coordinate system of images (top-left origin) with that of + * OpenGL (bottom-left origin). + * @returns {Matrix3} This matrix set to values described above. + */ +Matrix3.prototype.setToUnitYFlip = function () { + this[0] = 1; + this[1] = 0; + this[2] = 0; + this[3] = 0; + this[4] = -1; + this[5] = 1; + this[6] = 0; + this[7] = 0; + this[8] = 1; + return this; +}; + +/** + * Multiplies this matrix by a specified matrix. + * + * @param {Matrix3} matrix The matrix to multiply with this matrix. + * @returns {Matrix3} This matrix after multiplying it by the specified matrix. + * @throws {ArgumentError} if the specified matrix is null or undefined. + */ +Matrix3.prototype.multiplyMatrix = function (matrix) { + if (!matrix) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix3", "multiplyMatrix", "missingMatrix")); + } + + var ma = this, + mb = matrix, + ma0, ma1, ma2; + + // Row 1 + ma0 = ma[0]; + ma1 = ma[1]; + ma2 = ma[2]; + ma[0] = ma0 * mb[0] + ma1 * mb[3] + ma2 * mb[6]; + ma[1] = ma0 * mb[1] + ma1 * mb[4] + ma2 * mb[7]; + ma[2] = ma0 * mb[2] + ma1 * mb[5] + ma2 * mb[8]; + + // Row 2 + ma0 = ma[3]; + ma1 = ma[4]; + ma2 = ma[5]; + ma[3] = ma0 * mb[0] + ma1 * mb[3] + ma2 * mb[6]; + ma[4] = ma0 * mb[1] + ma1 * mb[4] + ma2 * mb[7]; + ma[5] = ma0 * mb[2] + ma1 * mb[5] + ma2 * mb[8]; + + // Row 3 + ma0 = ma[6]; + ma1 = ma[7]; + ma2 = ma[8]; + ma[6] = ma0 * mb[0] + ma1 * mb[3] + ma2 * mb[6]; + ma[7] = ma0 * mb[1] + ma1 * mb[4] + ma2 * mb[7]; + ma[8] = ma0 * mb[2] + ma1 * mb[5] + ma2 * mb[8]; + + return this; + +}; + +/** + * Multiplies this matrix by a matrix that transforms normalized coordinates from a source sector to a destination + * sector. Normalized coordinates within a sector range from 0 to 1, with (0, 0) indicating the lower left corner + * and (1, 1) indicating the upper right. The resultant matrix maps a normalized source coordinate (X, Y) to its + * corresponding normalized destination coordinate (X', Y'). + *
+ * This matrix typically necessary to transform texture coordinates from one geographic region to another. For + * example, the texture coordinates for a terrain tile spanning one region must be transformed to coordinates + * appropriate for an image tile spanning a potentially different region. + * + * @param {Sector} src the source sector + * @param {Sector} dst the destination sector + * + * @returns {Matrix3} this matrix multiplied by the transform matrix implied by values described above + */ +Matrix3.prototype.multiplyByTileTransform = function (src, dst) { + + var srcDeltaLat = src.deltaLatitude(); + var srcDeltaLon = src.deltaLongitude(); + var dstDeltaLat = dst.deltaLatitude(); + var dstDeltaLon = dst.deltaLongitude(); + + var xs = srcDeltaLon / dstDeltaLon; + var ys = srcDeltaLat / dstDeltaLat; + var xt = (src.minLongitude - dst.minLongitude) / dstDeltaLon; + var yt = (src.minLatitude - dst.minLatitude) / dstDeltaLat; + + // This is equivalent to the following operation, but is potentially much faster: + /*var m = new Matrix3( + xs, 0, xt, + 0, ys, yt, + 0, 0, 1); + this.multiplyMatrix(m);*/ + + // This inline version eliminates unnecessary multiplication by 1 and 0 in the matrix's components, reducing + // the total number of primitive operations from 63 to 18. + + var m = this; + + // Must be done before modifying m0, m1, etc. below. + m[2] += m[0] * xt + m[1] * yt; + m[5] += m[3] * xt + m[4] * yt; + m[8] += m[6] * xt + m[6] * yt; + + m[0] *= xs; + m[1] *= ys; + + m[3] *= xs; + m[4] *= ys; + + m[6] *= xs; + m[7] *= ys; + + return this; +}; + +/** + * Stores this matrix's components in column-major order in a specified array. + *
+ * The array must have space for at least 9 elements. This matrix's components are stored in the array
+ * starting with row 0 column 0 in index 0, row 1 column 0 in index 1, row 2 column 0 in index 2, and so on.
+ *
+ * @param {Float32Array | Float64Array | Number[]} result An array of at least 9 elements. Upon return,
+ * contains this matrix's components in column-major.
+ * @returns {Float32Array} The specified result array.
+ * @throws {ArgumentError} If the specified result array in null or undefined.
+ */
+Matrix3.prototype.columnMajorComponents = function (result) {
+ if (!result) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix3", "columnMajorComponents", "missingResult"));
+ }
+
+ // Column 1
+ result[0] = this[0];
+ result[1] = this[3];
+ result[2] = this[6];
+
+ // Column 2
+ result[3] = this[1];
+ result[4] = this[4];
+ result[5] = this[7];
+
+ // Column 3
+ result[6] = this[2];
+ result[7] = this[5];
+ result[8] = this[8];
+
+ return result;
+};
+
+export default Matrix3;
+
diff --git a/web/test/WebWorldWind/src/geom/Plane.js b/web/test/WebWorldWind/src/geom/Plane.js
new file mode 100644
index 00000000..8358db46
--- /dev/null
+++ b/web/test/WebWorldWind/src/geom/Plane.js
@@ -0,0 +1,287 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports Plane
+ */
+import ArgumentError from '../error/ArgumentError';
+import Line from '../geom/Line';
+import Logger from '../util/Logger';
+import Vec3 from '../geom/Vec3';
+
+
+/**
+ * Constructs a plane.
+ * This constructor does not normalize the components. It assumes that a unit normal vector is provided.
+ * @alias Plane
+ * @constructor
+ * @classdesc Represents a plane in Cartesian coordinates.
+ * The plane's X, Y and Z components indicate the plane's normal vector. The distance component
+ * indicates the plane's distance from the origin relative to its unit normal.
+ * The components are expected to be normalized.
+ * @param {Number} x The X coordinate of the plane's unit normal vector.
+ * @param {Number} y The Y coordinate of the plane's unit normal vector.
+ * @param {Number} z The Z coordinate of the plane's unit normal vector.
+ * @param {Number} distance The plane's distance from the origin.
+ */
+function Plane(x, y, z, distance) {
+ /**
+ * The normal vector to the plane.
+ * @type {Vec3}
+ */
+ this.normal = new Vec3(x, y, z);
+
+ /**
+ * The plane's distance from the origin.
+ * @type {Number}
+ */
+ this.distance = distance;
+}
+
+/**
+ * Computes a 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 {Vec3} pa The first point.
+ * @param {Vec3} pb The second point.
+ * @param {Vec3} pc The third point.
+ *
+ * @return {Plane} A plane passing through the specified points.
+ *
+ * @throws {ArgumentError} if pa, pb, or pc is null or undefined.
+ */
+Plane.fromPoints = function (pa, pb, pc) {
+ if (!pa || !pb || !pc) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Plane", "fromPoints", "missingVector"));
+ }
+
+ var vab = new Vec3(pb[0], pb[1], pb[2]);
+ vab.subtract(pa);
+ var vac = new Vec3(pc[0], pc[1], pc[2]);
+ vac.subtract(pa);
+ vab.cross(vac);
+ vab.normalize();
+ var d = -vab.dot(pa);
+
+ return new Plane(vab[0], vab[1], vab[2], d);
+};
+
+/**
+ * Computes the dot product of this plane's normal vector with a specified vector.
+ * Since the plane was defined with a unit normal vector, this function returns the distance of the vector from
+ * the plane.
+ * @param {Vec3} vector The vector to dot with this plane's normal vector.
+ * @returns {Number} The computed dot product.
+ * @throws {ArgumentError} If the specified vector is null or undefined.
+ */
+Plane.prototype.dot = function (vector) {
+ if (!vector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Plane", "dot", "missingVector"));
+ }
+
+ return this.normal.dot(vector) + this.distance;
+};
+
+/**
+ * Computes the distance between this plane and a point.
+ * @param {Vec3} point The point whose distance to compute.
+ * @returns {Number} The computed distance.
+ * @throws {ArgumentError} If the specified point is null or undefined.
+ */
+Plane.prototype.distanceToPoint = function (point) {
+ return this.dot(point);
+};
+
+/**
+ * Transforms this plane by a specified matrix.
+ * @param {Matrix} matrix The matrix to apply to this plane.
+ * @returns {Plane} This plane transformed by the specified matrix.
+ * @throws {ArgumentError} If the specified matrix is null or undefined.
+ */
+Plane.prototype.transformByMatrix = function (matrix) {
+ if (!matrix) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Plane", "transformByMatrix", "missingMatrix"));
+ }
+
+ var x = matrix[0] * this.normal[0] + matrix[1] * this.normal[1] + matrix[2] * this.normal[2] + matrix[3] * this.distance,
+ y = matrix[4] * this.normal[0] + matrix[5] * this.normal[1] + matrix[6] * this.normal[2] + matrix[7] * this.distance,
+ z = matrix[8] * this.normal[0] + matrix[9] * this.normal[1] + matrix[10] * this.normal[2] + matrix[11] * this.distance,
+ distance = matrix[12] * this.normal[0] + matrix[13] * this.normal[1] + matrix[14] * this.normal[2] + matrix[15] * this.distance;
+
+ this.normal[0] = x;
+ this.normal[1] = y;
+ this.normal[2] = z;
+ this.distance = distance;
+
+ return this;
+};
+
+/**
+ * Normalizes the components of this plane.
+ * @returns {Plane} This plane with its components normalized.
+ */
+Plane.prototype.normalize = function () {
+ var magnitude = this.normal.magnitude();
+
+ if (magnitude === 0)
+ return this;
+
+ this.normal.divide(magnitude);
+ this.distance /= magnitude;
+
+ return this;
+};
+
+/**
+ * Determines whether a specified line segment intersects this plane.
+ *
+ * @param {Vec3} endPoint1 The first end point of the line segment.
+ * @param {Vec3} endPoint2 The second end point of the line segment.
+ * @returns {Boolean} true if the line segment intersects this plane, otherwise false.
+ */
+Plane.prototype.intersectsSegment = function (endPoint1, endPoint2) {
+ var distance1 = this.dot(endPoint1),
+ distance2 = this.dot(endPoint2);
+
+ return distance1 * distance2 <= 0;
+};
+
+/**
+ * Computes the intersection point of this plane with a specified line segment.
+ *
+ * @param {Vec3} endPoint1 The first end point of the line segment.
+ * @param {Vec3} endPoint2 The second end point of the line segment.
+ * @param {Vec3} result A variable in which to return the intersection point of the line segment with this plane.
+ * @returns {Boolean} true If the line segment intersects this plane, otherwise false.
+ */
+Plane.prototype.intersectsSegmentAt = function (endPoint1, endPoint2, result) {
+ // Compute the distance from the end-points.
+ var distance1 = this.dot(endPoint1),
+ distance2 = this.dot(endPoint2);
+
+ // If both points points lie on the plane, ...
+ if (distance1 === 0 && distance2 === 0) {
+ // Choose an arbitrary endpoint as the intersection.
+ result[0] = endPoint1[0];
+ result[1] = endPoint1[1];
+ result[2] = endPoint1[2];
+
+ return true;
+ }
+ else if (distance1 === distance2) {
+ // The intersection is undefined.
+ return false;
+ }
+
+ var weight1 = -distance1 / (distance2 - distance1),
+ weight2 = 1 - weight1;
+
+ result[0] = weight1 * endPoint1[0] + weight2 * endPoint2[0];
+ result[1] = weight1 * endPoint1[1] + weight2 * endPoint2[1];
+ result[2] = weight1 * endPoint1[2] + weight2 * endPoint2[2];
+
+ return distance1 * distance2 <= 0;
+};
+
+/**
+ * Determines whether two points are on the same side of this plane.
+ *
+ * @param {Vec3} pointA the first point.
+ * @param {Vec3} pointB the second point.
+ *
+ * @return {Number} -1 If both points are on the negative side of this plane, +1 if both points are on the
+ * positive side of this plane, 0 if the points are on opposite sides of this plane.
+ *
+ * @throws {ArgumentError} If either point is null or undefined.
+ */
+Plane.prototype.onSameSide = function (pointA, pointB) {
+ if (!pointA || !pointB) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Plane", "onSameSide", "missingPoint"));
+ }
+
+ var da = this.distanceToPoint(pointA),
+ db = this.distanceToPoint(pointB);
+
+ if (da < 0 && db < 0)
+ return -1;
+
+ if (da > 0 && db > 0)
+ return 1;
+
+ return 0;
+};
+
+/**
+ * Clips a line segment to this plane.
+ * @param {Vec3} pointA The first line segment endpoint.
+ * @param {Vec3} pointB The second line segment endpoint.
+ *
+ * @returns {Vec3[]} 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 {ArgumentError} If either point is null or undefined.
+ */
+Plane.prototype.clip = function (pointA, pointB) {
+ if (!pointA || !pointB) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Plane", "clip", "missingPoint"));
+ }
+
+ if (pointA.equals(pointB)) {
+ return null;
+ }
+
+ // Get the projection of the segment onto the plane.
+ var line = Line.fromSegment(pointA, pointB),
+ lDotV = this.normal.dot(line.direction),
+ lDotS, t, p;
+
+ // Are the line and plane parallel?
+ if (lDotV === 0) { // line and plane are parallel and may be coincident.
+ lDotS = this.dot(line.origin);
+ if (lDotS === 0) {
+ return [pointA, pointB]; // 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?
+ t = -this.dot(line.origin) / lDotV; // lDotS / lDotV
+ if (t < 0 || t > 1) { // segment does not intersect
+ return null;
+ }
+
+ p = line.pointAt(t, new Vec3(0, 0, 0));
+ if (lDotV > 0) {
+ return [p, pointB];
+ } else {
+ return [pointA, p];
+ }
+};
+
+export default Plane;
diff --git a/web/test/WebWorldWind/src/geom/Position.js b/web/test/WebWorldWind/src/geom/Position.js
new file mode 100644
index 00000000..c59f1770
--- /dev/null
+++ b/web/test/WebWorldWind/src/geom/Position.js
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports Position
+ */
+import Angle from '../geom/Angle';
+import ArgumentError from '../error/ArgumentError';
+import Location from '../geom/Location';
+import Logger from '../util/Logger';
+import WWMath from '../util/WWMath';
+
+
+/**
+ * Constructs a position from a specified latitude and longitude in degrees and altitude in meters.
+ * @alias Position
+ * @constructor
+ * @classdesc Represents a latitude, longitude, altitude triple, with latitude and longitude in degrees and
+ * altitude in meters.
+ * @param {Number} latitude The latitude in degrees.
+ * @param {Number} longitude The longitude in degrees.
+ * @param {Number} altitude The altitude in meters.
+ */
+function Position(latitude, longitude, altitude) {
+ /**
+ * The latitude in degrees.
+ * @type {Number}
+ */
+ this.latitude = latitude;
+ /**
+ * The longitude in degrees.
+ * @type {Number}
+ */
+ this.longitude = longitude;
+ /**
+ * The altitude in meters.
+ * @type {Number}
+ */
+ this.altitude = altitude;
+}
+
+/**
+ * A Position with latitude, longitude and altitude all 0.
+ * @constant
+ * @type {Position}
+ */
+Position.ZERO = new Position(0, 0, 0);
+
+/**
+ * Creates a position from angles specified in radians.
+ * @param {Number} latitudeRadians The latitude in radians.
+ * @param {Number} longitudeRadians The longitude in radians.
+ * @param {Number} altitude The altitude in meters.
+ * @returns {Position} The new position with latitude and longitude in degrees.
+ */
+Position.fromRadians = function (latitudeRadians, longitudeRadians, altitude) {
+ return new Position(
+ latitudeRadians * Angle.RADIANS_TO_DEGREES,
+ longitudeRadians * Angle.RADIANS_TO_DEGREES,
+ altitude);
+};
+
+/**
+ * Sets this position to the latitude, longitude and altitude of a specified position.
+ * @param {Position} position The position to copy.
+ * @returns {Position} This position, set to the values of the specified position.
+ * @throws {ArgumentError} If the specified position is null or undefined.
+ */
+Position.prototype.copy = function (position) {
+ if (!position) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Position", "copy", "missingPosition"));
+ }
+
+ this.latitude = position.latitude;
+ this.longitude = position.longitude;
+ this.altitude = position.altitude;
+
+ return this;
+};
+
+/**
+ * Indicates whether this position has the same latitude, longitude and altitude as a specified position.
+ * @param {Position} position The position to compare with this one.
+ * @returns {Boolean} true if this position is equal to the specified one, otherwise false.
+ */
+Position.prototype.equals = function (position) {
+ return position
+ && position.latitude === this.latitude
+ && position.longitude === this.longitude
+ && position.altitude === this.altitude;
+};
+
+/**
+ * Computes a position along a great circle path at a specified distance between two specified positions.
+ * @param {Number} amount The fraction of the path between the two positions at which to compute the new
+ * position. This number should be between 0 and 1. If not, it is clamped to the nearest of those values.
+ * @param {Position} position1 The starting position.
+ * @param {Position} position2 The ending position.
+ * @param {Position} result A Position in which to return the result.
+ * @returns {Position} The specified result position.
+ * @throws {ArgumentError} If either specified position or the result argument is null or undefined.
+ */
+Position.interpolateGreatCircle = function (amount, position1, position2, result) {
+ if (!position1 || !position2) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Position", "interpolateGreatCircle", "missingPosition"));
+ }
+
+ if (!result) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Position", "interpolateGreatCircle", "missingResult"));
+ }
+
+ var t = WWMath.clamp(amount, 0, 1);
+ result.altitude = WWMath.interpolate(t, position1.altitude, position2.altitude);
+
+ //noinspection JSCheckFunctionSignatures
+ Location.interpolateGreatCircle(t, position1, position2, result);
+
+ return result;
+};
+
+/**
+ * Computes a position along a rhumb path at a specified distance between two specified positions.
+ * @param {Number} amount The fraction of the path between the two positions at which to compute the new
+ * position. This number should be between 0 and 1. If not, it is clamped to the nearest of those values.
+ * @param {Position} position1 The starting position.
+ * @param {Position} position2 The ending position.
+ * @param {Position} result A Position in which to return the result.
+ * @returns {Position} The specified result position.
+ * @throws {ArgumentError} If either specified position or the result argument is null or undefined.
+ */
+Position.interpolateRhumb = function (amount, position1, position2, result) {
+ if (!position1 || !position2) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Position", "interpolateRhumb", "missingPosition"));
+ }
+
+ if (!result) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Position", "interpolateRhumb", "missingResult"));
+ }
+
+ var t = WWMath.clamp(amount, 0, 1);
+ result.altitude = WWMath.interpolate(t, position1.altitude, position2.altitude);
+
+ //noinspection JSCheckFunctionSignatures
+ Location.interpolateRhumb(t, position1, position2, result);
+
+ return result;
+};
+
+/**
+ * Computes a position along a linear path at a specified distance between two specified positions.
+ * @param {Number} amount The fraction of the path between the two positions at which to compute the new
+ * position. This number should be between 0 and 1. If not, it is clamped to the nearest of those values.
+ * @param {Position} position1 The starting position.
+ * @param {Position} position2 The ending position.
+ * @param {Position} result A Position in which to return the result.
+ * @returns {Position} The specified result position.
+ * @throws {ArgumentError} If either specified position or the result argument is null or undefined.
+ */
+Position.interpolateLinear = function (amount, position1, position2, result) {
+ if (!position1 || !position2) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Position", "interpolateLinear", "missingPosition"));
+ }
+
+ if (!result) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Position", "interpolateLinear", "missingResult"));
+ }
+
+ var t = WWMath.clamp(amount, 0, 1);
+ result.altitude = WWMath.interpolate(t, position1.altitude, position2.altitude);
+
+ //noinspection JSCheckFunctionSignatures
+ Location.interpolateLinear(t, position1, position2, result);
+
+ return result;
+};
+
+/**
+ * Returns a string representation of this position.
+ * @returns {String}
+ */
+Position.prototype.toString = function () {
+ return "(" + this.latitude.toString() + "\u00b0, " + this.longitude.toString() + "\u00b0, "
+ + this.altitude.toString() + ")";
+};
+
+export default Position;
+
diff --git a/web/test/WebWorldWind/src/geom/Rectangle.js b/web/test/WebWorldWind/src/geom/Rectangle.js
new file mode 100644
index 00000000..47108e49
--- /dev/null
+++ b/web/test/WebWorldWind/src/geom/Rectangle.js
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports Rectangle
+ */
+
+/**
+ * Constructs a rectangle with a specified origin and size.
+ * @alias Rectangle
+ * @constructor
+ * @classdesc Represents a rectangle in 2D Cartesian coordinates.
+ * @param {Number} x The X coordinate of the rectangle's origin.
+ * @param {Number} y The Y coordinate of the rectangle's origin.
+ * @param {Number} width The rectangle's width.
+ * @param {Number} height The rectangle's height.
+ */
+function Rectangle(x, y, width, height) {
+
+ /**
+ * The X coordinate of this rectangle's origin.
+ * @type {Number}
+ */
+ this.x = x;
+
+ /**
+ * The Y coordinate of this rectangle's origin.
+ * @type {Number}
+ */
+ this.y = y;
+
+ /**
+ * This rectangle's width.
+ * @type {Number}
+ */
+ this.width = width;
+
+ /**
+ * This rectangle's height.
+ * @type {Number}
+ */
+ this.height = height;
+}
+
+/**
+ * Sets all this rectangle's properties.
+ * @param {Number} x The X coordinate of the rectangle's origin.
+ * @param {Number} y The Y coordinate of the rectangle's origin.
+ * @param {Number} width The rectangle's width.
+ * @param {Number} height The rectangle's height.
+ */
+Rectangle.prototype.set = function (x, y, width, height) {
+ this.x = x;
+ this.y = y;
+ this.width = width;
+ this.height = height;
+};
+
+/**
+ * Returns the minimum X value of this rectangle.
+ * @returns {Number} The rectangle's minimum X value.
+ */
+Rectangle.prototype.getMinX = function () {
+ return this.x;
+};
+
+/**
+ * Returns the minimum Y value of this rectangle.
+ * @returns {Number} The rectangle's minimum Y value.
+ */
+Rectangle.prototype.getMinY = function () {
+ return this.y;
+};
+
+/**
+ * Returns the maximum X value of this rectangle.
+ * @returns {Number} The rectangle's maximum X value.
+ */
+Rectangle.prototype.getMaxX = function () {
+ return this.x + this.width;
+};
+
+/**
+ * Returns the maximum Y value of this rectangle.
+ * @returns {Number} The rectangle's maximum Y value.
+ */
+Rectangle.prototype.getMaxY = function () {
+ return this.y + this.height;
+};
+
+/**
+ * Indicates whether this rectangle contains a specified point.
+ * @param {Vec2} point The point to test.
+ * @returns {Boolean} true if this rectangle contains the specified point, otherwise false.
+ */
+Rectangle.prototype.containsPoint = function (point) {
+ return point[0] >= this.x && point[0] <= this.x + this.width
+ && point[1] >= this.y && point[1] <= this.y + this.height;
+};
+/**
+ *
+ * Indicates whether this rectangle intersects a specified one.
+ * @param {Rectangle} that The rectangle to test.
+ * @returns {Boolean} true if this triangle and the specified one intersect, otherwise false.
+ */
+Rectangle.prototype.intersects = function (that) {
+ if (that.x + that.width < this.x)
+ return false;
+
+ if (that.x > this.x + this.width)
+ return false;
+
+ if (that.y + that.height < this.y)
+ return false;
+
+ //noinspection RedundantIfStatementJS
+ if (that.y > this.y + this.height)
+ return false;
+
+ return true;
+};
+
+/**
+ * Indicates whether this rectangle intersects any rectangle in a specified array of rectangles.
+ * @param {Rectangle[]} rectangles The rectangles to test intersection with.
+ * @returns {Boolean} true if this rectangle intersects any rectangle in the array, otherwise false.
+ */
+Rectangle.prototype.intersectsRectangles = function (rectangles) {
+ if (rectangles) {
+ for (var i = 0; i < rectangles.length; i++) {
+ if (this.intersects(rectangles[i])) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+};
+
+/**
+ * Returns a string representation of this object.
+ * @returns {String} A string representation of this object.
+ */
+Rectangle.prototype.toString = function () {
+ return this.x + ", " + this.y + ", " + this.width + ", " + this.height;
+};
+
+export default Rectangle;
diff --git a/web/test/WebWorldWind/src/geom/Sector.js b/web/test/WebWorldWind/src/geom/Sector.js
new file mode 100644
index 00000000..85aa40f8
--- /dev/null
+++ b/web/test/WebWorldWind/src/geom/Sector.js
@@ -0,0 +1,536 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports Sector
+ */
+import Angle from '../geom/Angle';
+import ArgumentError from '../error/ArgumentError';
+import Location from '../geom/Location';
+import Logger from '../util/Logger';
+import Vec3 from '../geom/Vec3';
+import WWMath from '../util/WWMath';
+
+
+/**
+ * Constructs a Sector from specified minimum and maximum latitudes and longitudes in degrees.
+ * @alias Sector
+ * @constructor
+ * @classdesc Represents a rectangular region in geographic coordinates in degrees.
+ * @param {Number} minLatitude The sector's minimum latitude in degrees.
+ * @param {Number} maxLatitude The sector's maximum latitude in degrees.
+ * @param {Number} minLongitude The sector's minimum longitude in degrees.
+ * @param {Number} maxLongitude The sector's maximum longitude in degrees.
+ */
+function Sector(minLatitude, maxLatitude, minLongitude, maxLongitude) {
+ /**
+ * This sector's minimum latitude in degrees.
+ * @type {Number}
+ */
+ this.minLatitude = minLatitude;
+ /**
+ * This sector's maximum latitude in degrees.
+ * @type {Number}
+ */
+ this.maxLatitude = maxLatitude;
+ /**
+ * This sector's minimum longitude in degrees.
+ * @type {Number}
+ */
+ this.minLongitude = minLongitude;
+ /**
+ * This sector's maximum longitude in degrees.
+ * @type {Number}
+ */
+ this.maxLongitude = maxLongitude;
+}
+
+/**
+ * A sector with minimum and maximum latitudes and minimum and maximum longitudes all zero.
+ * @constant
+ * @type {Sector}
+ */
+Sector.ZERO = new Sector(0, 0, 0, 0);
+
+/**
+ * A sector that encompasses the full range of latitude ([-90, 90]) and longitude ([-180, 180]).
+ * @constant
+ * @type {Sector}
+ */
+Sector.FULL_SPHERE = new Sector(-WWMath.MAX_LAT, WWMath.MAX_LAT, -180, 180);
+
+/**
+ * Sets this sector's latitudes and longitudes to those of a specified sector.
+ * @param {Sector} sector The sector to copy.
+ * @returns {Sector} This sector, set to the values of the specified sector.
+ * @throws {ArgumentError} If the specified sector is null or undefined.
+ */
+Sector.prototype.copy = function (sector) {
+ if (!sector) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Sector", "copy", "missingSector"));
+ }
+
+ this.minLatitude = sector.minLatitude;
+ this.maxLatitude = sector.maxLatitude;
+ this.minLongitude = sector.minLongitude;
+ this.maxLongitude = sector.maxLongitude;
+
+ return this;
+};
+
+/**
+ * Indicates whether this sector has width or height.
+ * @returns {Boolean} true if this sector's minimum and maximum latitudes or minimum and maximum
+ * longitudes do not differ, otherwise false.
+ */
+Sector.prototype.isEmpty = function () {
+ return this.minLatitude === this.maxLatitude && this.minLongitude === this.maxLongitude;
+};
+
+/**
+ * Returns the angle between this sector's minimum and maximum latitudes, in degrees.
+ * @returns {Number} The difference between this sector's minimum and maximum latitudes, in degrees.
+ */
+Sector.prototype.deltaLatitude = function () {
+ return this.maxLatitude - this.minLatitude;
+};
+
+/**
+ * Returns the angle between this sector's minimum and maximum longitudes, in degrees.
+ * @returns {Number} The difference between this sector's minimum and maximum longitudes, in degrees.
+ */
+Sector.prototype.deltaLongitude = function () {
+ return this.maxLongitude - this.minLongitude;
+};
+
+/**
+ * Returns the angle midway between this sector's minimum and maximum latitudes.
+ * @returns {Number} The mid-angle of this sector's minimum and maximum latitudes, in degrees.
+ */
+Sector.prototype.centroidLatitude = function () {
+ return 0.5 * (this.minLatitude + this.maxLatitude);
+};
+
+/**
+ * Returns the angle midway between this sector's minimum and maximum longitudes.
+ * @returns {Number} The mid-angle of this sector's minimum and maximum longitudes, in degrees.
+ */
+Sector.prototype.centroidLongitude = function () {
+ return 0.5 * (this.minLongitude + this.maxLongitude);
+};
+
+/**
+ * Computes the location of the angular center of this sector, which is the mid-angle of each of this sector's
+ * latitude and longitude dimensions.
+ * @param {Location} result A pre-allocated {@link Location} in which to return the computed centroid.
+ * @returns {Location} The specified result argument containing the computed centroid.
+ * @throws {ArgumentError} If the result argument is null or undefined.
+ */
+Sector.prototype.centroid = function (result) {
+ if (!result) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Sector", "centroid", "missingResult"));
+ }
+
+ result.latitude = this.centroidLatitude();
+ result.longitude = this.centroidLongitude();
+
+ return result;
+};
+
+/**
+ * Returns this sector's minimum latitude in radians.
+ * @returns {Number} This sector's minimum latitude in radians.
+ */
+Sector.prototype.minLatitudeRadians = function () {
+ return this.minLatitude * Angle.DEGREES_TO_RADIANS;
+};
+
+/**
+ * Returns this sector's maximum latitude in radians.
+ * @returns {Number} This sector's maximum latitude in radians.
+ */
+Sector.prototype.maxLatitudeRadians = function () {
+ return this.maxLatitude * Angle.DEGREES_TO_RADIANS;
+};
+
+/**
+ * Returns this sector's minimum longitude in radians.
+ * @returns {Number} This sector's minimum longitude in radians.
+ */
+Sector.prototype.minLongitudeRadians = function () {
+ return this.minLongitude * Angle.DEGREES_TO_RADIANS;
+};
+
+/**
+ * Returns this sector's maximum longitude in radians.
+ * @returns {Number} This sector's maximum longitude in radians.
+ */
+Sector.prototype.maxLongitudeRadians = function () {
+ return this.maxLongitude * Angle.DEGREES_TO_RADIANS;
+};
+
+/**
+ * Modifies this sector to encompass an array of specified locations.
+ * @param {Location[]} locations An array of locations. The array may be sparse.
+ * @returns {Sector} This sector, modified to encompass all locations in the specified array.
+ * @throws {ArgumentError} If the specified array is null, undefined or empty or has fewer than two locations.
+ */
+Sector.prototype.setToBoundingSector = function (locations) {
+ if (!locations || locations.length < 2) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Sector", "setToBoundingSector",
+ "missingArray"));
+ }
+
+ var minLatitude = 90,
+ maxLatitude = -90,
+ minLongitude = 180,
+ maxLongitude = -180;
+
+ for (var idx = 0, len = locations.length; idx < len; idx += 1) {
+ var location = locations[idx];
+
+ if (!location) {
+ continue;
+ }
+
+ minLatitude = Math.min(minLatitude, location.latitude);
+ maxLatitude = Math.max(maxLatitude, location.latitude);
+ minLongitude = Math.min(minLongitude, location.longitude);
+ maxLongitude = Math.max(maxLongitude, location.longitude);
+ }
+
+ this.minLatitude = minLatitude;
+ this.maxLatitude = maxLatitude;
+ this.minLongitude = minLongitude;
+ this.maxLongitude = maxLongitude;
+
+ return this;
+};
+
+/**
+ * Computes bounding sectors from a list of locations that span the dateline.
+ * @param {Location[]} locations The locations to bound.
+ * @returns {Sector[]} Two sectors, one in the eastern hemisphere and one in the western hemisphere.
+ * Returns null if the computed bounding sector has zero width or height.
+ * @throws {ArgumentError} If the specified array is null, undefined or empty or the number of locations
+ * is less than 2.
+ */
+Sector.splitBoundingSectors = function (locations) {
+ if (!locations || locations.length < 2) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Sector", "splitBoundingSectors",
+ "missingArray"));
+ }
+
+ var minLat = 90;
+ var minLon = 180;
+ var maxLat = -90;
+ var maxLon = -180;
+
+ var lastLocation = null;
+
+ for (var idx = 0, len = locations.length; idx < len; idx += 1) {
+ var location = locations[idx];
+
+ var lat = location.latitude;
+ if (lat < minLat) {
+ minLat = lat;
+ }
+ if (lat > maxLat) {
+ maxLat = lat;
+ }
+
+ var lon = location.longitude;
+ if (lon >= 0 && lon < minLon) {
+ minLon = lon;
+ }
+ if (lon <= 0 && lon > maxLon) {
+ maxLon = lon;
+ }
+
+ if (lastLocation != null) {
+ var lastLon = lastLocation.longitude;
+ if (WWMath.signum(lon) != WWMath.signum(lastLon)) {
+ if (Math.abs(lon - lastLon) < 180) {
+ // Crossing the zero longitude line too
+ maxLon = 0;
+ minLon = 0;
+ }
+ }
+ }
+ lastLocation = location;
+ }
+
+ if (minLat === maxLat && minLon === maxLon) {
+ return null;
+ }
+
+ return [
+ new Sector(minLat, maxLat, minLon, 180), // Sector on eastern hemisphere.
+ new Sector(minLat, maxLat, -180, maxLon) // Sector on western hemisphere.
+ ];
+};
+
+/**
+ * Indicates whether this sector intersects a specified sector.
+ * This sector intersects the specified sector when each sector's boundaries either overlap with the specified
+ * sector or are adjacent to the specified sector.
+ * The sectors are assumed to have normalized angles (angles within the range [-90, 90] latitude and
+ * [-180, 180] longitude).
+ * @param {Sector} sector The sector to test intersection with. May be null or undefined, in which case this
+ * function returns false.
+ * @returns {Boolean} true if the specifies sector intersections this sector, otherwise false.
+ */
+Sector.prototype.intersects = function (sector) {
+ // Assumes normalized angles: [-90, 90], [-180, 180].
+ return sector
+ && this.minLongitude <= sector.maxLongitude
+ && this.maxLongitude >= sector.minLongitude
+ && this.minLatitude <= sector.maxLatitude
+ && this.maxLatitude >= sector.minLatitude;
+};
+
+/**
+ * Indicates whether this sector intersects a specified sector exclusive of the sector boundaries.
+ * This sector overlaps the specified sector when the union of the two sectors defines a non-empty sector.
+ * The sectors are assumed to have normalized angles (angles within the range [-90, 90] latitude and
+ * [-180, 180] longitude).
+ * @param {Sector} sector The sector to test overlap with. May be null or undefined, in which case this
+ * function returns false.
+ * @returns {Boolean} true if the specified sector overlaps this sector, otherwise false.
+ */
+Sector.prototype.overlaps = function (sector) {
+ // Assumes normalized angles: [-90, 90], [-180, 180].
+ return sector
+ && this.minLongitude < sector.maxLongitude
+ && this.maxLongitude > sector.minLongitude
+ && this.minLatitude < sector.maxLatitude
+ && this.maxLatitude > sector.minLatitude;
+};
+
+/**
+ * Indicates whether this sector fully contains a specified sector.
+ * This sector contains the specified sector when the specified sector's boundaries are completely contained
+ * within this sector's boundaries, or are equal to this sector's boundaries.
+ * The sectors are assumed to have normalized angles (angles within the range [-90, 90] latitude and
+ * [-180, 180] longitude).
+ * @param {Sector} sector The sector to test containment with. May be null or undefined, in which case this
+ * function returns false.
+ * @returns {Boolean} true if the specified sector contains this sector, otherwise false.
+ */
+Sector.prototype.contains = function (sector) {
+ // Assumes normalized angles: [-90, 90], [-180, 180].
+ return sector
+ && this.minLatitude <= sector.minLatitude
+ && this.maxLatitude >= sector.maxLatitude
+ && this.minLongitude <= sector.minLongitude
+ && this.maxLongitude >= sector.maxLongitude;
+};
+
+/**
+ * Indicates whether this sector contains a specified geographic location.
+ * @param {Number} latitude The location's latitude in degrees.
+ * @param {Number} longitude The location's longitude in degrees.
+ * @returns {Boolean} true if this sector contains the location, otherwise false.
+ */
+Sector.prototype.containsLocation = function (latitude, longitude) {
+ // Assumes normalized angles: [-90, 90], [-180, 180].
+ return this.minLatitude <= latitude
+ && this.maxLatitude >= latitude
+ && this.minLongitude <= longitude
+ && this.maxLongitude >= longitude;
+};
+
+/**
+ * Sets this sector to the intersection of itself and a specified sector.
+ * @param {Sector} sector The sector to intersect with this one.
+ * @returns {Sector} This sector, set to its intersection with the specified sector.
+ * @throws {ArgumentError} If the specified sector is null or undefined.
+ */
+Sector.prototype.intersection = function (sector) {
+ if (!sector instanceof Sector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Sector", "intersection", "missingSector"));
+ }
+
+ // Assumes normalized angles: [-180, 180], [-90, 90].
+ if (this.minLatitude < sector.minLatitude)
+ this.minLatitude = sector.minLatitude;
+ if (this.maxLatitude > sector.maxLatitude)
+ this.maxLatitude = sector.maxLatitude;
+ if (this.minLongitude < sector.minLongitude)
+ this.minLongitude = sector.minLongitude;
+ if (this.maxLongitude > sector.maxLongitude)
+ this.maxLongitude = sector.maxLongitude;
+
+ // If the sectors do not overlap in either latitude or longitude, then the result of the above logic results in
+ // the max being greater than the min. In this case, set the max to indicate that the sector is empty in
+ // that dimension.
+ if (this.maxLatitude < this.minLatitude)
+ this.maxLatitude = this.minLatitude;
+ if (this.maxLongitude < this.minLongitude)
+ this.maxLongitude = this.minLongitude;
+
+ return this;
+};
+
+/**
+ * Returns a list of the Lat/Lon coordinates of a Sector's corners.
+ *
+ * @returns {Array} an array of the four corner locations, in the order SW, SE, NE, NW
+ */
+Sector.prototype.getCorners = function () {
+ var corners = [];
+
+ corners.push(new Location(this.minLatitude, this.minLongitude));
+ corners.push(new Location(this.minLatitude, this.maxLongitude));
+ corners.push(new Location(this.maxLatitude, this.maxLongitude));
+ corners.push(new Location(this.maxLatitude, this.minLongitude));
+
+ return corners;
+};
+
+/**
+ * Returns an array of {@link Vec3} that bounds the specified sector on the surface of the specified
+ * {@link Globe}. The returned points enclose the globe's surface terrain in the sector,
+ * according to the specified vertical exaggeration, minimum elevation, and maximum elevation. If the minimum and
+ * maximum elevation are equal, this assumes a maximum elevation of 10 + the minimum.
+ *
+ * @param {Globe} globe the globe the extent relates to.
+ * @param {Number} verticalExaggeration the globe's vertical surface exaggeration.
+ *
+ * @returns {Vec3} a set of points that enclose the globe's surface on the specified sector. Can be turned into a {@link BoundingBox}
+ * with the setToVec3Points method.
+ *
+ * @throws {ArgumentError} if the globe is null.
+ */
+Sector.prototype.computeBoundingPoints = function (globe, verticalExaggeration) {
+ // TODO: Refactor this method back to computeBoundingBox.
+ // This method was originally computeBoundingBox and returned a BoundingBox. This created a circular dependency between
+ // Sector and BoundingBox that the Karma unit test suite doesn't appear to like. If we discover a way to make Karma handle this
+ // situation, we should refactor this method.
+ if (globe === null) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Sector", "computeBoundingBox", "missingGlobe"));
+ }
+
+ var minAndMaxElevations = globe.minAndMaxElevationsForSector(this);
+
+ // Compute the exaggerated minimum and maximum heights.
+ var minHeight = minAndMaxElevations[0] * verticalExaggeration;
+ var maxHeight = minAndMaxElevations[1] * verticalExaggeration;
+
+ if (minHeight === maxHeight)
+ maxHeight = minHeight + 10; // Ensure the top and bottom heights are not equal.
+
+ var points = [];
+ var corners = this.getCorners();
+ for (var i = 0; i < corners.length; i++) {
+ points.push(globe.computePointFromPosition(corners[i].latitude, corners[i].longitude, minHeight, new Vec3(0, 0, 0)));
+ points.push(globe.computePointFromPosition(corners[i].latitude, corners[i].longitude, maxHeight, new Vec3(0, 0, 0)));
+ }
+
+ // A point at the centroid captures the maximum vertical dimension.
+ var centroid = this.centroid(new Location(0, 0));
+ points.push(globe.computePointFromPosition(centroid.latitude, centroid.longitude, maxHeight, new Vec3(0, 0, 0)));
+
+ // If the sector spans the equator, then the curvature of all four edges need to be taken into account. The
+ // extreme points along the top and bottom edges are located at their mid-points, and the extreme points along
+ // the left and right edges are on the equator. Add points with the longitude of the sector's centroid but with
+ // the sector's min and max latitude, and add points with the sector's min and max longitude but with latitude
+ // at the equator. See WWJINT-225.
+ if (this.minLatitude < 0 && this.maxLatitude > 0) {
+ points.push(globe.computePointFromPosition(this.minLatitude, centroid.longitude, maxHeight, new Vec3(0, 0, 0)));
+ points.push(globe.computePointFromPosition(this.maxLatitude, centroid.longitude, maxHeight, new Vec3(0, 0, 0)));
+ points.push(globe.computePointFromPosition(0, this.minLongitude, maxHeight, new Vec3(0, 0, 0)));
+ points.push(globe.computePointFromPosition(0, this.maxLongitude, maxHeight, new Vec3(0, 0, 0)));
+ }
+ // If the sector is located entirely in the southern hemisphere, then the curvature of its top edge needs to be
+ // taken into account. The extreme point along the top edge is located at its mid-point. Add a point with the
+ // longitude of the sector's centroid but with the sector's max latitude. See WWJINT-225.
+ else if (this.minLatitude < 0) {
+ points.push(globe.computePointFromPosition(this.maxLatitude, centroid.longitude, maxHeight, new Vec3(0, 0, 0)));
+ }
+ // If the sector is located entirely in the northern hemisphere, then the curvature of its bottom edge needs to
+ // be taken into account. The extreme point along the bottom edge is located at its mid-point. Add a point with
+ // the longitude of the sector's centroid but with the sector's min latitude. See WWJINT-225.
+ else {
+ points.push(globe.computePointFromPosition(this.minLatitude, centroid.longitude, maxHeight, new Vec3(0, 0, 0)));
+ }
+
+ // If the sector spans 360 degrees of longitude then is a band around the entire globe. (If one edge is a pole
+ // then the sector looks like a circle around the pole.) Add points at the min and max latitudes and longitudes
+ // 0, 180, 90, and -90 to capture full extent of the band.
+ if (this.deltaLongitude() >= 360) {
+ var minLat = this.minLatitude;
+ points.push(globe.computePointFromPosition(minLat, 0, maxHeight, new Vec3(0, 0, 0)));
+ points.push(globe.computePointFromPosition(minLat, 90, maxHeight, new Vec3(0, 0, 0)));
+ points.push(globe.computePointFromPosition(minLat, -90, maxHeight, new Vec3(0, 0, 0)));
+ points.push(globe.computePointFromPosition(minLat, 180, maxHeight, new Vec3(0, 0, 0)));
+
+ var maxLat = this.maxLatitude;
+ points.push(globe.computePointFromPosition(maxLat, 0, maxHeight, new Vec3(0, 0, 0)));
+ points.push(globe.computePointFromPosition(maxLat, 90, maxHeight, new Vec3(0, 0, 0)));
+ points.push(globe.computePointFromPosition(maxLat, -90, maxHeight, new Vec3(0, 0, 0)));
+ points.push(globe.computePointFromPosition(maxLat, 180, maxHeight, new Vec3(0, 0, 0)));
+ }
+ else if (this.deltaLongitude() > 180) {
+ // Need to compute more points to ensure the box encompasses the full sector.
+ var cLon = centroid.longitude;
+ var cLat = centroid.latitude;
+
+ // centroid latitude, longitude midway between min longitude and centroid longitude
+ var lon = (this.minLongitude + cLon) / 2;
+ points.push(globe.computePointFromPosition(cLat, lon, maxHeight, new Vec3(0, 0, 0)));
+
+ // centroid latitude, longitude midway between centroid longitude and max longitude
+ lon = (cLon + this.maxLongitude) / 2;
+ points.push(globe.computePointFromPosition(cLat, lon, maxHeight, new Vec3(0, 0, 0)));
+
+ // centroid latitude, longitude at min longitude and max longitude
+ points.push(globe.computePointFromPosition(cLat, this.minLongitude, maxHeight, new Vec3(0, 0, 0)));
+ points.push(globe.computePointFromPosition(cLat, this.maxLongitude, maxHeight, new Vec3(0, 0, 0)));
+ }
+
+ return points;
+};
+
+/**
+ * Sets this sector to the union of itself and a specified sector.
+ * @param {Sector} sector The sector to union with this one.
+ * @returns {Sector} This sector, set to its union with the specified sector.
+ * @throws {ArgumentError} if the specified sector is null or undefined.
+ */
+Sector.prototype.union = function (sector) {
+ if (!sector instanceof Sector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Sector", "union", "missingSector"));
+ }
+
+ // Assumes normalized angles: [-180, 180], [-90, 90].
+ if (this.minLatitude > sector.minLatitude)
+ this.minLatitude = sector.minLatitude;
+ if (this.maxLatitude < sector.maxLatitude)
+ this.maxLatitude = sector.maxLatitude;
+ if (this.minLongitude > sector.minLongitude)
+ this.minLongitude = sector.minLongitude;
+ if (this.maxLongitude < sector.maxLongitude)
+ this.maxLongitude = sector.maxLongitude;
+
+ return this;
+};
+
+export default Sector;
diff --git a/web/test/WebWorldWind/src/geom/Vec2.js b/web/test/WebWorldWind/src/geom/Vec2.js
new file mode 100644
index 00000000..3840ea2a
--- /dev/null
+++ b/web/test/WebWorldWind/src/geom/Vec2.js
@@ -0,0 +1,321 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import Logger from '../util/Logger';
+import ArgumentError from '../error/ArgumentError';
+import Vec3 from '../geom/Vec3';
+
+
+/**
+ * Constructs a two-component vector.
+ * @alias Vec2
+ * @classdesc Represents a two-component vector. Access the X component of the vector as v[0] and the Y
+ * component as v[1].
+ * @augments Float64Array
+ * @param {Number} x X component of vector.
+ * @param {Number} y Y component of vector.
+ * @constructor
+ */
+function Vec2(x, y) {
+ this[0] = x;
+ this[1] = y;
+}
+
+// Vec2 inherits from Float64Array.
+Vec2.prototype = new Float64Array(2);
+
+/**
+ * Assigns the components of this vector.
+ * @param {Number} x The X component of the vector.
+ * @param {Number} y The Y component of the vector.
+ * @returns {Vec2} This vector with the specified components assigned.
+ */
+Vec2.prototype.set = function (x, y) {
+ this[0] = x;
+ this[1] = y;
+
+ return this;
+};
+
+/**
+ * Copies the components of a specified vector to this vector.
+ * @param {Vec2} vector The vector to copy.
+ * @returns {Vec2} This vector set to the values of the specified vector.
+ * @throws {ArgumentError} If the specified vector is null or undefined.
+ */
+Vec2.prototype.copy = function (vector) {
+ if (!vector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec2", "copy", "missingVector"));
+ }
+
+ this[0] = vector[0];
+ this[1] = vector[1];
+
+ return this;
+};
+
+/**
+ * Indicates whether the X and Y components of this vector are identical to those of a specified vector.
+ * @param {Vec2} vector The vector to test.
+ * @returns {Boolean} true if this vector's components are equal to those of the specified vector,
+ * otherwise false.
+ */
+Vec2.prototype.equals = function (vector) {
+ return this[0] === vector[0] && this[1] === vector[1];
+};
+
+/**
+ * Computes the average of a specified array of vectors.
+ * @param {Vec2[]} vectors The vectors whose average to compute.
+ * @param {Vec2} result A pre-allocated Vec2 in which to return the computed average.
+ * @returns {Vec2} The result argument set to the average of the specified lists of vectors.
+ * @throws {ArgumentError} If the specified array of vectors is null, undefined or empty, or the specified
+ * result argument is null or undefined.
+ */
+Vec2.average = function (vectors, result) {
+ if (!vectors || vectors.length < 1) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec2", "average", "missingArray"));
+ }
+
+ if (!result) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec2", "average", "missingResult"));
+ }
+
+ var count = vectors.length,
+ vec;
+
+ result[0] = 0;
+ result[1] = 0;
+
+ for (var i = 0, len = vectors.length; i < len; i++) {
+ vec = vectors[i];
+
+ result[0] += vec[0] / count;
+ result[1] += vec[1] / count;
+ }
+
+ return result;
+};
+
+/**
+ * Adds a vector to this vector.
+ * @param {Vec2} addend The vector to add to this one.
+ * @returns {Vec2} This vector after adding the specified vector to it.
+ * @throws {ArgumentError} If the specified addend is null or undefined.
+ */
+Vec2.prototype.add = function (addend) {
+ if (!addend) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec2", "add", "missingVector"));
+ }
+
+ this[0] += addend[0];
+ this[1] += addend[1];
+
+ return this;
+};
+
+/**
+ * Subtracts a vector from this vector.
+ * @param {Vec2} subtrahend The vector to subtract from this one.
+ * @returns {Vec2} This vector after subtracting the specified vector from it.
+ * @throws {ArgumentError} If the subtrahend is null or undefined.
+ */
+Vec2.prototype.subtract = function (subtrahend) {
+ if (!subtrahend) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec2", "subtract", "missingVector"));
+ }
+
+ this[0] -= subtrahend[0];
+ this[1] -= subtrahend[1];
+
+ return this;
+};
+
+/**
+ * Multiplies this vector by a scalar.
+ * @param {Number} scalar The scalar to multiply this vector by.
+ * @returns {Vec2} This vector multiplied by the specified scalar.
+ */
+Vec2.prototype.multiply = function (scalar) {
+ this[0] *= scalar;
+ this[1] *= scalar;
+
+ return this;
+};
+
+/**
+ * Divide this vector by a scalar.
+ * @param {Number} divisor The scalar to divide this vector by.
+ * @returns {Vec2} This vector divided by the specified scalar.
+ */
+Vec2.prototype.divide = function (divisor) {
+ this[0] /= divisor;
+ this[1] /= divisor;
+
+ return this;
+};
+
+/**
+ * Mixes (interpolates) a specified vector with this vector, modifying this vector.
+ * @param {Vec2} vector The vector to mix.
+ * @param {Number} weight The relative weight of this vector.
+ * @returns {Vec2} This vector modified to the mix of itself and the specified vector.
+ * @throws {ArgumentError} If the specified vector is null or undefined.
+ */
+Vec2.prototype.mix = function (vector, weight) {
+ if (!vector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec2", "mix", "missingVector"));
+ }
+
+ var w0 = 1 - weight,
+ w1 = weight;
+
+ this[0] = this[0] * w0 + vector[0] * w1;
+ this[1] = this[1] * w0 + vector[1] * w1;
+
+ return this;
+};
+
+/**
+ * Negates this vector.
+ * @returns {Vec2} This vector, negated.
+ */
+Vec2.prototype.negate = function () {
+ this[0] = -this[0];
+ this[1] = -this[1];
+
+ return this;
+};
+
+/**
+ * Computes the scalar dot product of this vector and a specified vector.
+ * @param {Vec2} vector The vector to multiply.
+ * @returns {Number} The scalar dot product of the vectors.
+ * @throws {ArgumentError} If the specified vector is null or undefined.
+ */
+Vec2.prototype.dot = function (vector) {
+ if (!vector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec2", "dot", "missingVector"));
+ }
+
+ return this[0] * vector[0] + this[1] * vector[1];
+};
+
+/**
+ * Computes the squared magnitude of this vector.
+ * @returns {Number} The squared magnitude of this vector.
+ */
+Vec2.prototype.magnitudeSquared = function () {
+ return this.dot(this);
+};
+
+/**
+ * Computes the magnitude of this vector.
+ * @returns {Number} The magnitude of this vector.
+ */
+Vec2.prototype.magnitude = function () {
+ return Math.sqrt(this.magnitudeSquared());
+};
+
+/**
+ * Normalizes this vector to a unit vector.
+ * @returns {Vec2} This vector, normalized.
+ */
+Vec2.prototype.normalize = function () {
+ var magnitude = this.magnitude(),
+ magnitudeInverse = 1 / magnitude;
+
+ this[0] *= magnitudeInverse;
+ this[1] *= magnitudeInverse;
+
+ return this;
+};
+
+/**
+ * Computes the squared distance from this vector to a specified vector.
+ * @param {Vec2} vector The vector to compute the distance to.
+ * @returns {Number} The squared distance between the vectors.
+ * @throws {ArgumentError} If the specified vector is null or undefined.
+ */
+Vec2.prototype.distanceToSquared = function (vector) {
+ if (!vector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec2", "distanceToSquared", "missingVector"));
+ }
+
+ var dx = this[0] - vector[0],
+ dy = this[1] - vector[1];
+
+ return dx * dx + dy * dy;
+};
+
+/**
+ * Computes the distance from this vector to a specified vector.
+ * @param {Vec2} vector The vector to compute the distance to.
+ * @returns {Number} The distance between the vectors.
+ * @throws {ArgumentError} If the specified vector is null or undefined.
+ */
+Vec2.prototype.distanceTo = function (vector) {
+ if (!vector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec2", "distanceTo", "missingVector"));
+ }
+
+ return Math.sqrt(this.distanceToSquared(vector));
+};
+
+/**
+ * Creates a {@link Vec3} using this vector's X and Y components and a Z component of 0.
+ * @returns {Vec3} A new vector whose X and Y components are those of this vector and whose Z component is 0.
+ */
+Vec2.prototype.toVec3 = function () {
+ return new Vec3(this[0], this[1], 0);
+};
+
+/**
+ * Swaps the components of this vector with those of another vector. This vector is set to the values of the
+ * specified vector, and the specified vector's components are set to the values of this vector.
+ * @param {Vec2} that The vector to swap.
+ * @returns {Vec2} This vector set to the values of the specified vector.
+ */
+Vec2.prototype.swap = function (that) {
+ var tmp = this[0];
+ this[0] = that[0];
+ that[0] = tmp;
+
+ tmp = this[1];
+ this[1] = that[1];
+ that[1] = tmp;
+
+ return this;
+};
+
+/**
+ * Returns a string representation of this vector.
+ * @returns {String} A string representation of this vector, in the form "(x, y)".
+ */
+Vec2.prototype.toString = function () {
+ return "(" + this[0] + ", " + this[1] + ")";
+};
+
+export default Vec2;
diff --git a/web/test/WebWorldWind/src/geom/Vec3.js b/web/test/WebWorldWind/src/geom/Vec3.js
new file mode 100644
index 00000000..44c4091f
--- /dev/null
+++ b/web/test/WebWorldWind/src/geom/Vec3.js
@@ -0,0 +1,538 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import Logger from '../util/Logger';
+import ArgumentError from '../error/ArgumentError';
+
+
+/**
+ * Constructs a three-component vector.
+ * @alias Vec3
+ * @classdesc Represents a three-component vector. Access the X component of the vector as v[0], the Y
+ * component as v[1] and the Z component as v[2].
+ * @augments Float64Array
+ * @param {Number} x X component of vector.
+ * @param {Number} y Y component of vector.
+ * @param {Number} z Z component of vector.
+ * @constructor
+ */
+function Vec3(x, y, z) {
+ this[0] = x;
+ this[1] = y;
+ this[2] = z;
+}
+
+// Vec3 extends Float64Array.
+Vec3.prototype = new Float64Array(3);
+
+/**
+ * A vector corresponding to the origin.
+ * @type {Vec3}
+ */
+Vec3.ZERO = new Vec3(0, 0, 0);
+
+/**
+ * Computes the average of a specified array of vectors.
+ * @param {Vec3[]} vectors The vectors whose average to compute.
+ * @param {Vec3} result A pre-allocated Vec3 in which to return the computed average.
+ * @returns {Vec3} The result argument set to the average of the specified array of vectors.
+ * @throws {ArgumentError} If the specified array of vectors is null, undefined or empty or the specified
+ * result argument is null or undefined.
+ */
+Vec3.average = function (vectors, result) {
+ if (!vectors || vectors.length < 1) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec3", "average", "missingArray"));
+ }
+
+ if (!result) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec3", "average", "missingResult"));
+ }
+
+ var count = vectors.length,
+ vec;
+
+ result[0] = 0;
+ result[1] = 0;
+ result[2] = 0;
+
+ for (var i = 0, len = vectors.length; i < len; i++) {
+ vec = vectors[i];
+
+ result[0] += vec[0] / count;
+ result[1] += vec[1] / count;
+ result[2] += vec[2] / count;
+ }
+
+ return result;
+};
+
+/**
+ * Computes the average of a specified array of points packed into a single array.
+ * @param {Float32Array | Float64Array | Number[]} points The points whose average to compute.
+ * @param {Vec3} result A pre-allocated Vec3 in which to return the computed average.
+ * @returns {Vec3} The result argument set to the average of the specified array of points.
+ * @throws {ArgumentError} If the specified array of points is null, undefined or empty or the result argument
+ * is null or undefined.
+ */
+Vec3.averageOfBuffer = function (points, result) {
+ if (!points || points.length < 1) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec3", "averageBuffer", "missingArray"));
+ }
+
+ if (!result) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec3", "averageBuffer", "missingResult"));
+ }
+
+ var count = points.length / 3;
+
+ result[0] = 0;
+ result[1] = 0;
+ result[2] = 0;
+
+ for (var i = 0; i < count; i++) {
+ result[0] += points[i * 3] / count;
+ result[1] += points[i * 3 + 1] / count;
+ result[2] += points[i * 3 + 2] / count;
+ }
+
+ return result;
+};
+
+/**
+ * Indicates whether three vectors are colinear.
+ * @param {Vec3} a The first vector.
+ * @param {Vec3} b The second vector.
+ * @param {Vec3} c The third vector.
+ * @returns {Boolean} true if the vectors are colinear, otherwise false.
+ * @throws {ArgumentError} If any of the specified vectors are null or undefined.
+ */
+Vec3.areColinear = function (a, b, c) {
+ if (!a || !b || !c) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec3", "areColinear", "missingVector"));
+ }
+
+ var ab = new Vec3(a[0] - b[0], a[1] - b[1], a[2] - b[2]).normalize(),
+ bc = new Vec3(c[0] - b[0], c[1] - b[1], c[2] - b[2]).normalize();
+
+ // ab and bc are considered colinear if their dot product is near +/-1.
+ return Math.abs(ab.dot(bc)) > 0.999;
+};
+
+/**
+ * Computes the normal vector of a specified triangle.
+ *
+ * @param {Vec3} a The triangle's first vertex.
+ * @param {Vec3} b The triangle's second vertex.
+ * @param {Vec3} c The triangle's third vertex.
+ * @returns {Vec3} The triangle's unit-normal vector.
+ * @throws {ArgumentError} If any of the specified vectors are null or undefined.
+ */
+Vec3.computeTriangleNormal = function (a, b, c) {
+ if (!a || !b || !c) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec3", "areColinear", "missingVector"));
+ }
+
+ var x = (b[1] - a[1]) * (c[2] - a[2]) - (b[2] - a[2]) * (c[1] - a[1]),
+ y = (b[2] - a[2]) * (c[0] - a[0]) - (b[0] - a[0]) * (c[2] - a[2]),
+ z = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]),
+ length = x * x + y * y + z * z;
+
+ if (length === 0) {
+ return new Vec3(x, y, z);
+ }
+
+ length = Math.sqrt(length);
+
+ return new Vec3(x / length, y / length, z / length);
+};
+
+/**
+ * Finds three non-colinear points in an array of coordinates.
+ *
+ * @param {Number[]} coords The coordinates, in the order x0, y0, z0, x1, y1, z1, ...
+ * @param {Number} stride The number of numbers between successive points. 0 indicates that the points
+ * are arranged one immediately after the other, as would the value 3.
+ * @returns {Vec3[]} Three non-colinear points from the input array of coordinates, or null if three
+ * non-colinear points could not be found or the specified coordinates array is null, undefined or
+ * contains fewer than three points.
+ */
+Vec3.findThreeIndependentVertices = function (coords, stride) {
+ var xstride = stride && stride > 0 ? stride : 3;
+
+ if (!coords || coords.length < 3 * xstride) {
+ return null;
+ }
+
+ var a = new Vec3(coords[0], coords[1], coords[2]),
+ b = null,
+ c = null,
+ k = xstride;
+
+ for (; k < coords.length; k += xstride) {
+ b = new Vec3(coords[k], coords[k + 1], coords[k + 2]);
+ if (!(b[0] === a[0] && b[1] === a[1] && b[2] === a[2])) {
+ break;
+ }
+ b = null;
+ }
+
+ if (!b) {
+ return null;
+ }
+
+ for (k += xstride; k < coords.length; k += xstride) {
+ c = new Vec3(coords[k], coords[k + 1], coords[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[0] === a[0] && c[1] === a[1] && c[2] === a[2]
+ || c[0] === b[0] && c[1] === b[1] && c[2] === b[2])) {
+ if (!Vec3.areColinear(a, b, c))
+ break;
+ }
+
+ c = null;
+ }
+
+ return c ? [a, b, c] : null;
+};
+
+/**
+ * Computes a unit-normal vector for a buffer of coordinate triples. The normal vector is computed from the
+ * first three non-colinear points in the buffer.
+ *
+ * @param {Number[]} coords The coordinates, in the order x0, y0, z0, x1, y1, z1, ...
+ * @param {Number} stride The number of numbers between successive points. 0 indicates that the points
+ * are arranged one immediately after the other, as would the value 3.
+ * @returns {Vec3} The computed unit-length normal vector.
+ */
+Vec3.computeBufferNormal = function (coords, stride) {
+ var vertices = Vec3.findThreeIndependentVertices(coords, stride);
+
+ return vertices ? Vec3.computeTriangleNormal(vertices[0], vertices[1], vertices[2]) : null;
+};
+
+/**
+ * Assigns the components of this vector.
+ * @param {Number} x The X component of the vector.
+ * @param {Number} y The Y component of the vector.
+ * @param {Number} z The Z component of the vector.
+ * @returns {Vec3} This vector with the specified components assigned.
+ */
+Vec3.prototype.set = function (x, y, z) {
+ this[0] = x;
+ this[1] = y;
+ this[2] = z;
+
+ return this;
+};
+
+/**
+ * Copies the components of a specified vector to this vector.
+ * @param {Vec3} vector The vector to copy.
+ * @returns {Vec3} This vector set to the X, Y and Z values of the specified vector.
+ * @throws {ArgumentError} If the specified vector is null or undefined.
+ */
+Vec3.prototype.copy = function (vector) {
+ if (!vector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec3", "copy", "missingVector"));
+ }
+
+ this[0] = vector[0];
+ this[1] = vector[1];
+ this[2] = vector[2];
+
+ return this;
+};
+
+/**
+ * Indicates whether the components of this vector are identical to those of a specified vector.
+ * @param {Vec3} vector The vector to test.
+ * @returns {Boolean} true if the components of this vector are equal to those of the specified one,
+ * otherwise false.
+ */
+Vec3.prototype.equals = function (vector) {
+ return this[0] === vector[0] && this[1] === vector[1] && this[2] === vector[2];
+};
+
+/**
+ * Adds a specified vector to this vector.
+ * @param {Vec3} addend The vector to add.
+ * @returns {Vec3} This vector after adding the specified vector to it.
+ * @throws {ArgumentError} If the addend is null or undefined.
+ */
+Vec3.prototype.add = function (addend) {
+ if (!addend) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec3", "add", "missingVector"));
+ }
+
+ this[0] += addend[0];
+ this[1] += addend[1];
+ this[2] += addend[2];
+
+ return this;
+};
+
+/**
+ * Subtracts a specified vector from this vector.
+ * @param {Vec3} subtrahend The vector to subtract
+ * @returns {Vec3} This vector after subtracting the specified vector from it.
+ * @throws {ArgumentError} If the subtrahend is null or undefined.
+ */
+Vec3.prototype.subtract = function (subtrahend) {
+ if (!subtrahend) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec3", "subtract", "missingVector"));
+ }
+
+ this[0] -= subtrahend[0];
+ this[1] -= subtrahend[1];
+ this[2] -= subtrahend[2];
+
+ return this;
+};
+
+/**
+ * Multiplies this vector by a scalar.
+ * @param {Number} scalar The scalar to multiply this vector by.
+ * @returns {Vec3} This vector multiplied by the specified scalar.
+ */
+Vec3.prototype.multiply = function (scalar) {
+ this[0] *= scalar;
+ this[1] *= scalar;
+ this[2] *= scalar;
+
+ return this;
+};
+
+/**
+ * Divides this vector by a scalar.
+ * @param {Number} divisor The scalar to divide this vector by.
+ * @returns {Vec3} This vector divided by the specified scalar.
+ */
+Vec3.prototype.divide = function (divisor) {
+ this[0] /= divisor;
+ this[1] /= divisor;
+ this[2] /= divisor;
+
+ return this;
+};
+
+/**
+ * Multiplies this vector by a 4x4 matrix. The multiplication is performed with an implicit W component of 1.
+ * The resultant W component of the product is then divided through the X, Y, and Z components.
+ *
+ * @param {Matrix} matrix The matrix to multiply this vector by.
+ * @returns {Vec3} This vector multiplied by the specified matrix.
+ * @throws ArgumentError If the specified matrix is null or undefined.
+ */
+Vec3.prototype.multiplyByMatrix = function (matrix) {
+ if (!matrix) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec3", "multiplyByMatrix", "missingMatrix"));
+ }
+
+ var x = matrix[0] * this[0] + matrix[1] * this[1] + matrix[2] * this[2] + matrix[3],
+ y = matrix[4] * this[0] + matrix[5] * this[1] + matrix[6] * this[2] + matrix[7],
+ z = matrix[8] * this[0] + matrix[9] * this[1] + matrix[10] * this[2] + matrix[11],
+ w = matrix[12] * this[0] + matrix[13] * this[1] + matrix[14] * this[2] + matrix[15];
+
+ this[0] = x / w;
+ this[1] = y / w;
+ this[2] = z / w;
+
+ return this;
+};
+
+/**
+ * Mixes (interpolates) a specified vector with this vector, modifying this vector.
+ * @param {Vec3} vector The vector to mix with this one.
+ * @param {Number} weight The relative weight of this vector.
+ * @returns {Vec3} This vector modified to the mix of itself and the specified vector.
+ * @throws {ArgumentError} If the specified vector is null or undefined.
+ */
+Vec3.prototype.mix = function (vector, weight) {
+ if (!vector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec3", "mix", "missingVector"));
+ }
+
+ var w0 = 1 - weight,
+ w1 = weight;
+
+ this[0] = this[0] * w0 + vector[0] * w1;
+ this[1] = this[1] * w0 + vector[1] * w1;
+ this[2] = this[2] * w0 + vector[2] * w1;
+
+ return this;
+};
+
+/**
+ * Negates the components of this vector.
+ * @returns {Vec3} This vector, negated.
+ */
+Vec3.prototype.negate = function () {
+ this[0] = -this[0];
+ this[1] = -this[1];
+ this[2] = -this[2];
+
+ return this;
+};
+
+/**
+ * Computes the scalar dot product of this vector and a specified vector.
+ * @param {Vec3} vector The vector to multiply.
+ * @returns {Number} The dot product of the two vectors.
+ * @throws {ArgumentError} If the specified vector is null or undefined.
+ */
+Vec3.prototype.dot = function (vector) {
+ if (!vector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec3", "dot", "missingVector"));
+ }
+
+ return this[0] * vector[0] +
+ this[1] * vector[1] +
+ this[2] * vector[2];
+};
+
+/**
+ * Computes the cross product of this vector and a specified vector, modifying this vector.
+ * @param {Vec3} vector The vector to cross with this vector.
+ * @returns {Vec3} This vector set to the cross product of itself and the specified vector.
+ * @throws {ArgumentError} If the specified vector is null or undefined.
+ */
+Vec3.prototype.cross = function (vector) {
+ if (!vector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec3", "cross", "missingVector"));
+ }
+
+ var x = this[1] * vector[2] - this[2] * vector[1],
+ y = this[2] * vector[0] - this[0] * vector[2],
+ z = this[0] * vector[1] - this[1] * vector[0];
+
+ this[0] = x;
+ this[1] = y;
+ this[2] = z;
+
+ return this;
+};
+
+/**
+ * Computes the squared magnitude of this vector.
+ * @returns {Number} The squared magnitude of this vector.
+ */
+Vec3.prototype.magnitudeSquared = function () {
+ return this.dot(this);
+};
+
+/**
+ * Computes the magnitude of this vector.
+ * @returns {Number} The magnitude of this vector.
+ */
+Vec3.prototype.magnitude = function () {
+ return Math.sqrt(this.magnitudeSquared());
+};
+
+/**
+ * Normalizes this vector to a unit vector.
+ * @returns {Vec3} This vector, normalized.
+ */
+Vec3.prototype.normalize = function () {
+ var magnitude = this.magnitude(),
+ magnitudeInverse = 1 / magnitude;
+
+ this[0] *= magnitudeInverse;
+ this[1] *= magnitudeInverse;
+ this[2] *= magnitudeInverse;
+
+ return this;
+};
+
+/**
+ * Computes the squared distance from this vector to a specified vector.
+ * @param {Vec3} vector The vector to compute the distance to.
+ * @returns {Number} The squared distance between the vectors.
+ * @throws {ArgumentError} If the specified vector is null or undefined.
+ */
+Vec3.prototype.distanceToSquared = function (vector) {
+ if (!vector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec3", "distanceToSquared", "missingVector"));
+ }
+
+ var dx = this[0] - vector[0],
+ dy = this[1] - vector[1],
+ dz = this[2] - vector[2];
+
+ return dx * dx + dy * dy + dz * dz;
+};
+
+/**
+ * Computes the distance from this vector to another vector.
+ * @param {Vec3} vector The vector to compute the distance to.
+ * @returns {number} The distance between the vectors.
+ * @throws {ArgumentError} If the specified vector is null or undefined.
+ */
+Vec3.prototype.distanceTo = function (vector) {
+ if (!vector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Vec3", "distanceTo", "missingVector"));
+ }
+
+ return Math.sqrt(this.distanceToSquared(vector));
+};
+
+/**
+ * Swaps this vector with that vector. This vector's components are set to the values of the specified
+ * vector's components, and the specified vector's components are set to the values of this vector's components.
+ * @param {Vec3} that The vector to swap.
+ * @returns {Vec3} This vector set to the values of the specified vector.
+ */
+Vec3.prototype.swap = function (that) {
+ var tmp = this[0];
+ this[0] = that[0];
+ that[0] = tmp;
+
+ tmp = this[1];
+ this[1] = that[1];
+ that[1] = tmp;
+
+ tmp = this[2];
+ this[2] = that[2];
+ that[2] = tmp;
+
+ return this;
+};
+
+/**
+ * Returns a string representation of this vector.
+ * @returns {String} A string representation of this vector, in the form "(x, y, z)".
+ */
+Vec3.prototype.toString = function () {
+ return "(" + this[0] + ", " + this[1] + ", " + this[2] + ")";
+};
+
+export default Vec3;
diff --git a/web/test/WebWorldWind/src/gesture/ClickRecognizer.js b/web/test/WebWorldWind/src/gesture/ClickRecognizer.js
new file mode 100644
index 00000000..c9b0a348
--- /dev/null
+++ b/web/test/WebWorldWind/src/gesture/ClickRecognizer.js
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports ClickRecognizer
+ */
+import GestureRecognizer from '../gesture/GestureRecognizer';
+
+
+/**
+ * Constructs a mouse click gesture recognizer.
+ * @alias ClickRecognizer
+ * @constructor
+ * @augments GestureRecognizer
+ * @classdesc A concrete gesture recognizer subclass that looks for single or multiple mouse clicks.
+ * @param {EventTarget} target The document element this gesture recognizer observes for mouse and touch events.
+ * @param {Function} callback An optional function to call when this gesture is recognized. If non-null, the
+ * function is called when this gesture is recognized, and is passed a single argument: this gesture recognizer,
+ * e.g., gestureCallback(recognizer).
+ * @throws {ArgumentError} If the specified target is null or undefined.
+ */
+function ClickRecognizer(target, callback) {
+ GestureRecognizer.call(this, target, callback);
+
+ /**
+ *
+ * @type {Number}
+ */
+ this.numberOfClicks = 1;
+
+ /**
+ *
+ * @type {Number}
+ */
+ this.button = 0;
+
+ // Intentionally not documented.
+ this.maxMouseMovement = 5;
+
+ // Intentionally not documented.
+ this.maxClickDuration = 500;
+
+ // Intentionally not documented.
+ this.maxClickInterval = 400;
+
+ // Intentionally not documented.
+ this.clicks = [];
+
+ // Intentionally not documented.
+ this.timeout = null;
+}
+
+ClickRecognizer.prototype = Object.create(GestureRecognizer.prototype);
+
+// Documented in superclass.
+ClickRecognizer.prototype.reset = function () {
+ GestureRecognizer.prototype.reset.call(this);
+
+ this.clicks = [];
+ this.cancelFailAfterDelay();
+};
+
+// Documented in superclass.
+ClickRecognizer.prototype.mouseDown = function (event) {
+ if (this.state != WorldWind.POSSIBLE) {
+ return;
+ }
+
+ if (this.button != event.button) {
+ this.state = WorldWind.FAILED;
+ } else {
+ var click = {
+ clientX: this.clientX,
+ clientY: this.clientY
+ };
+ this.clicks.push(click);
+ this.failAfterDelay(this.maxClickDuration); // fail if the click is down too long
+ }
+};
+
+// Documented in superclass.
+ClickRecognizer.prototype.mouseMove = function (event) {
+ if (this.state != WorldWind.POSSIBLE) {
+ return;
+ }
+
+ var dx = this.translationX,
+ dy = this.translationY,
+ distance = Math.sqrt(dx * dx + dy * dy);
+ if (distance > this.maxMouseMovement) {
+ this.state = WorldWind.FAILED;
+ }
+};
+
+// Documented in superclass.
+ClickRecognizer.prototype.mouseUp = function (event) {
+ if (this.state != WorldWind.POSSIBLE) {
+ return;
+ }
+
+ if (this.mouseButtonMask != 0) {
+ return; // wait until the last button is up
+ }
+
+ var clickCount = this.clicks.length;
+ if (clickCount == this.numberOfClicks) {
+ this.clientX = this.clicks[0].clientX;
+ this.clientY = this.clicks[0].clientY;
+ this.state = WorldWind.RECOGNIZED;
+ } else {
+ this.failAfterDelay(this.maxClickInterval); // fail if the interval between clicks is too long
+ }
+};
+
+// Documented in superclass.
+ClickRecognizer.prototype.touchStart = function (touch) {
+ if (this.state != WorldWind.POSSIBLE) {
+ return;
+ }
+
+ this.state = WorldWind.FAILED; // mouse gestures fail upon receiving a touch event
+};
+
+// Intentionally not documented.
+ClickRecognizer.prototype.failAfterDelay = function (delay) {
+ var self = this;
+ if (self.timeout) {
+ window.clearTimeout(self.timeout);
+ }
+
+ self.timeout = window.setTimeout(function () {
+ self.timeout = null;
+ if (self.state == WorldWind.POSSIBLE) {
+ self.state = WorldWind.FAILED; // fail if we haven't already reached a terminal state
+ }
+ }, delay);
+};
+
+// Intentionally not documented.
+ClickRecognizer.prototype.cancelFailAfterDelay = function () {
+ var self = this;
+ if (self.timeout) {
+ window.clearTimeout(self.timeout);
+ self.timeout = null;
+ }
+};
+
+export default ClickRecognizer;
+
diff --git a/web/test/WebWorldWind/src/gesture/DragRecognizer.js b/web/test/WebWorldWind/src/gesture/DragRecognizer.js
new file mode 100644
index 00000000..621642bd
--- /dev/null
+++ b/web/test/WebWorldWind/src/gesture/DragRecognizer.js
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports DragRecognizer
+ */
+import GestureRecognizer from '../gesture/GestureRecognizer';
+
+
+/**
+ * Constructs a mouse drag gesture recognizer.
+ * @alias DragRecognizer
+ * @constructor
+ * @augments GestureRecognizer
+ * @classdesc A concrete gesture recognizer subclass that looks for mouse drag gestures.
+ * @param {EventTarget} target The document element this gesture recognizer observes for mouse and touch events.
+ * @param {Function} callback An optional function to call when this gesture is recognized. If non-null, the
+ * function is called when this gesture is recognized, and is passed a single argument: this gesture recognizer,
+ * e.g., gestureCallback(recognizer).
+ * @throws {ArgumentError} If the specified target is null or undefined.
+ */
+function DragRecognizer(target, callback) {
+ GestureRecognizer.call(this, target, callback);
+
+ /**
+ *
+ * @type {Number}
+ */
+ this.button = 0;
+
+ // Intentionally not documented.
+ this.interpretDistance = 5;
+}
+
+DragRecognizer.prototype = Object.create(GestureRecognizer.prototype);
+
+// Documented in superclass.
+DragRecognizer.prototype.mouseMove = function (event) {
+ if (this.state == WorldWind.POSSIBLE) {
+ if (this.shouldInterpret()) {
+ if (this.shouldRecognize()) {
+ this.translationX = 0; // set translation to zero when the drag begins
+ this.translationY = 0;
+ this.state = WorldWind.BEGAN;
+ } else {
+ this.state = WorldWind.FAILED;
+ }
+ }
+ } else if (this.state == WorldWind.BEGAN || this.state == WorldWind.CHANGED) {
+ this.state = WorldWind.CHANGED;
+ }
+};
+
+// Documented in superclass.
+DragRecognizer.prototype.mouseUp = function (event) {
+ if (this.mouseButtonMask == 0) { // last button up
+ if (this.state == WorldWind.POSSIBLE) {
+ this.state = WorldWind.FAILED;
+ } else if (this.state == WorldWind.BEGAN || this.state == WorldWind.CHANGED) {
+ this.state = WorldWind.ENDED;
+ }
+ }
+};
+
+// Documented in superclass.
+DragRecognizer.prototype.touchStart = function (touch) {
+ if (this.state == WorldWind.POSSIBLE) {
+ this.state = WorldWind.FAILED; // mouse gestures fail upon receiving a touch event
+ }
+};
+
+/**
+ *
+ * @returns {Boolean}
+ * @protected
+ */
+DragRecognizer.prototype.shouldInterpret = function () {
+ var dx = this.translationX,
+ dy = this.translationY,
+ distance = Math.sqrt(dx * dx + dy * dy);
+ return distance > this.interpretDistance; // interpret mouse movement when the cursor moves far enough
+};
+
+/**
+ *
+ * @returns {Boolean}
+ * @protected
+ */
+DragRecognizer.prototype.shouldRecognize = function () {
+ var buttonBit = 1 << this.button;
+ return buttonBit == this.mouseButtonMask; // true when the specified button is the only button down
+};
+
+export default DragRecognizer;
+
diff --git a/web/test/WebWorldWind/src/gesture/GestureRecognizer.js b/web/test/WebWorldWind/src/gesture/GestureRecognizer.js
new file mode 100644
index 00000000..6e9ea35e
--- /dev/null
+++ b/web/test/WebWorldWind/src/gesture/GestureRecognizer.js
@@ -0,0 +1,793 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports GestureRecognizer
+ */
+import ArgumentError from '../error/ArgumentError';
+import Logger from '../util/Logger';
+import Touch from '../gesture/Touch';
+
+
+/**
+ * Constructs a base gesture recognizer. This is an abstract base class and not intended to be instantiated
+ * directly.
+ * @alias GestureRecognizer
+ * @constructor
+ * @classdesc Gesture recognizers translate user input event streams into higher level actions. A gesture
+ * recognizer is associated with an event target, which dispatches mouse and keyboard events to the gesture
+ * recognizer. When a gesture recognizer has received enough information from the event stream to interpret the
+ * action, it calls its callback functions. Callback functions may be specified at construction or added to the
+ * [gestureCallbacks]{@link GestureRecognizer#gestureCallbacks} list after construction.
+ * @param {EventTarget} target The document element this gesture recognizer observes for mouse and touch events.
+ * @param {Function} callback An optional function to call when this gesture is recognized. If non-null, the
+ * function is called when this gesture is recognized, and is passed a single argument: this gesture recognizer,
+ * e.g., gestureCallback(recognizer).
+ * @throws {ArgumentError} If the specified target is null or undefined.
+ */
+// TODO: evaluate target usage
+function GestureRecognizer(target, callback) {
+ if (!target) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "GestureRecognizer", "constructor", "missingTarget"));
+ }
+
+ /**
+ * Indicates the document element this gesture recognizer observes for UI events.
+ * @type {EventTarget}
+ * @readonly
+ */
+ this.target = target;
+
+ /**
+ * Indicates whether or not this gesture recognizer is enabled. When false, this gesture recognizer will
+ * ignore any events dispatched by its target.
+ * @type {Boolean}
+ * @default true
+ */
+ this.enabled = true;
+
+ // Documented with its property accessor below.
+ this._state = WorldWind.POSSIBLE;
+
+ // Intentionally not documented.
+ this._nextState = null;
+
+ // Documented with its property accessor below.
+ this._clientX = 0;
+
+ // Documented with its property accessor below.
+ this._clientY = 0;
+
+ // Intentionally not documented.
+ this._clientStartX = 0;
+
+ // Intentionally not documented.
+ this._clientStartY = 0;
+
+ // Documented with its property accessor below.
+ this._translationX = 0;
+
+ // Documented with its property accessor below.
+ this._translationY = 0;
+
+ // Intentionally not documented.
+ this._translationWeight = 0.4;
+
+ // Documented with its property accessor below.
+ this._mouseButtonMask = 0;
+
+ // Intentionally not documented.
+ this._touches = [];
+
+ // Intentionally not documented.
+ this._touchCentroidShiftX = 0;
+
+ // Intentionally not documented.
+ this._touchCentroidShiftY = 0;
+
+ // Documented with its property accessor below.
+ this._gestureCallbacks = [];
+
+ // Intentionally not documented.
+ this._canRecognizeWith = [];
+
+ // Intentionally not documented.
+ this._requiresFailureOf = [];
+
+ // Intentionally not documented.
+ this._requiredToFailBy = [];
+
+ // Add the optional gesture callback.
+ if (callback) {
+ this._gestureCallbacks.push(callback);
+ }
+
+ // Intentionally not documented.
+ this.listenerList = [];
+
+ // Add this recognizer to the list of all recognizers.
+ GestureRecognizer.allRecognizers.push(this);
+}
+
+// Intentionally not documented.
+GestureRecognizer.allRecognizers = [];
+
+Object.defineProperties(GestureRecognizer.prototype, {
+ /**
+ * Indicates this gesture's current state. Possible values are WorldWind.POSSIBLE, WorldWind.FAILED,
+ * WorldWind.RECOGNIZED, WorldWind.BEGAN, WorldWind.CHANGED, WorldWind.CANCELLED and WorldWind.ENDED.
+ * @type {String}
+ * @default WorldWind.POSSIBLE
+ * @memberof GestureRecognizer.prototype
+ */
+ state: {
+ get: function () {
+ return this._state;
+ },
+ set: function (value) {
+ this.transitionToState(value);
+ }
+ },
+
+ /**
+ * Indicates the X coordinate of this gesture.
+ * @type {Number}
+ * @memberof GestureRecognizer.prototype
+ */
+ clientX: {
+ get: function () {
+ return this._clientX;
+ },
+ set: function (value) {
+ this._clientX = value;
+ }
+ },
+
+ /**
+ * Returns the Y coordinate of this gesture.
+ * @type {Number}
+ * @memberof GestureRecognizer.prototype
+ */
+ clientY: {
+ get: function () {
+ return this._clientY;
+ },
+ set: function (value) {
+ this._clientY = value;
+ }
+ },
+
+ /**
+ * Indicates this gesture's translation along the X axis since the gesture started.
+ * @type {Number}
+ * @memberof GestureRecognizer.prototype
+ */
+ translationX: {
+ get: function () {
+ return this._translationX;
+ },
+ set: function (value) {
+ this._translationX = value;
+ this._clientStartX = this._clientX;
+ this._touchCentroidShiftX = 0;
+ }
+ },
+
+ /**
+ * Indicates this gesture's translation along the Y axis since the gesture started.
+ * @type {Number}
+ * @memberof GestureRecognizer.prototype
+ */
+ translationY: {
+ get: function () {
+ return this._translationY;
+ },
+ set: function (value) {
+ this._translationY = value;
+ this._clientStartY = this._clientY;
+ this._touchCentroidShiftY = 0;
+ }
+ },
+
+ /**
+ * Indicates the currently pressed mouse buttons as a bitmask. A value of 0 indicates that no buttons are
+ * pressed. A nonzero value indicates that one or more buttons are pressed as follows: bit 1 indicates the
+ * primary button, bit 2 indicates the the auxiliary button, bit 3 indicates the secondary button.
+ * @type {Number}
+ * @readonly
+ * @memberof GestureRecognizer.prototype
+ */
+ mouseButtonMask: {
+ get: function () {
+ return this._mouseButtonMask;
+ }
+ },
+
+ /**
+ * Indicates the number of active touches.
+ * @type {Number}
+ * @readonly
+ * @memberof GestureRecognizer.prototype
+ */
+ touchCount: {
+ get: function () {
+ return this._touches.length;
+ }
+ },
+
+ /**
+ * The list of functions to call when this gesture is recognized. The functions have a single argument:
+ * this gesture recognizer, e.g., gestureCallback(recognizer). Applications may
+ * add functions to this array or remove them.
+ * @type {Function[]}
+ * @readonly
+ * @memberof GestureRecognizer.prototype
+ */
+ gestureCallbacks: {
+ get: function () {
+ return this._gestureCallbacks;
+ }
+ }
+});
+
+/**
+ *
+ * @param index
+ * @returns {Touch}
+ * @throws {ArgumentError} If the index is out of range.
+ */
+GestureRecognizer.prototype.touch = function (index) {
+ if (index < 0 || index >= this._touches.length) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "GestureRecognizer", "touch", "indexOutOfRange"));
+ }
+
+ return this._touches[index];
+};
+
+/**
+ *
+ * @param recognizer
+ */
+GestureRecognizer.prototype.recognizeSimultaneouslyWith = function (recognizer) {
+ if (!recognizer) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "GestureRecognizer", "recognizeSimultaneouslyWith",
+ "The specified gesture recognizer is null or undefined."));
+ }
+
+ var index = this._canRecognizeWith.indexOf(recognizer);
+ if (index == -1) {
+ this._canRecognizeWith.push(recognizer);
+ recognizer._canRecognizeWith.push(this);
+ }
+};
+
+/**
+ *
+ * @param recognizer
+ * @returns {Boolean}
+ */
+GestureRecognizer.prototype.canRecognizeSimultaneouslyWith = function (recognizer) {
+ var index = this._canRecognizeWith.indexOf(recognizer);
+ return index != -1;
+};
+
+/**
+ *
+ * @param recognizer
+ */
+GestureRecognizer.prototype.requireRecognizerToFail = function (recognizer) {
+ if (!recognizer) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "GestureRecognizer", "requireRecognizerToFail",
+ "The specified gesture recognizer is null or undefined"));
+ }
+
+ var index = this._requiresFailureOf.indexOf(recognizer);
+ if (index == -1) {
+ this._requiresFailureOf.push(recognizer);
+ recognizer._requiredToFailBy.push(this);
+ }
+};
+
+/**
+ *
+ * @param recognizer
+ * @returns {Boolean}
+ */
+GestureRecognizer.prototype.requiresRecognizerToFail = function (recognizer) {
+ var index = this._requiresFailureOf.indexOf(recognizer);
+ return index != -1;
+};
+
+/**
+ *
+ * @param recognizer
+ * @returns {Boolean}
+ */
+GestureRecognizer.prototype.requiredToFailByRecognizer = function (recognizer) {
+ var index = this._requiredToFailBy.indexOf(recognizer);
+ return index != -1;
+};
+
+/**
+ * @protected
+ */
+GestureRecognizer.prototype.reset = function () {
+ this._state = WorldWind.POSSIBLE;
+ this._nextState = null;
+ this._clientX = 0;
+ this._clientY = 0;
+ this._clientStartX = 0;
+ this._clientStartY = 0;
+ this._translationX = 0;
+ this._translationY = 0;
+ this._mouseButtonMask = 0;
+ this._touches = [];
+ this._touchCentroidShiftX = 0;
+ this._touchCentroidShiftY = 0;
+};
+
+/**
+ * @protected
+ */
+GestureRecognizer.prototype.prepareToRecognize = function () {
+};
+
+/**
+ *
+ * @param event
+ * @protected
+ */
+GestureRecognizer.prototype.mouseDown = function (event) {
+};
+
+/**
+ *
+ * @param event
+ * @protected
+ */
+GestureRecognizer.prototype.mouseMove = function (event) {
+};
+
+/**
+ *
+ * @param event
+ * @protected
+ */
+GestureRecognizer.prototype.mouseUp = function (event) {
+};
+
+/**
+ *
+ * @param touch
+ * @protected
+ */
+GestureRecognizer.prototype.touchStart = function (touch) {
+};
+
+/**
+ *
+ * @param touch
+ * @protected
+ */
+GestureRecognizer.prototype.touchMove = function (touch) {
+};
+
+/**
+ *
+ * @param touch
+ * @protected
+ */
+GestureRecognizer.prototype.touchCancel = function (touch) {
+};
+
+/**
+ *
+ * @param touch
+ * @protected
+ */
+GestureRecognizer.prototype.touchEnd = function (touch) {
+};
+
+// Intentionally not documented.
+GestureRecognizer.prototype.transitionToState = function (newState) {
+ this._nextState = null; // clear any pending state transition
+
+ if (newState === WorldWind.FAILED) {
+ this._state = newState;
+ this.updateRecognizersWaitingForFailure();
+ this.resetIfEventsEnded();
+ } else if (newState === WorldWind.RECOGNIZED) {
+ this.tryToRecognize(newState); // may prevent the transition to Recognized
+ if (this._state === newState) {
+ this.prepareToRecognize();
+ this.notifyListeners();
+ this.callGestureCallbacks();
+ this.resetIfEventsEnded();
+ }
+ } else if (newState === WorldWind.BEGAN) {
+ this.tryToRecognize(newState); // may prevent the transition to Began
+ if (this._state === newState) {
+ this.prepareToRecognize();
+ this.notifyListeners();
+ this.callGestureCallbacks();
+ }
+ } else if (newState === WorldWind.CHANGED) {
+ this._state = newState;
+ this.notifyListeners();
+ this.callGestureCallbacks();
+ } else if (newState === WorldWind.CANCELLED) {
+ this._state = newState;
+ this.notifyListeners();
+ this.callGestureCallbacks();
+ this.resetIfEventsEnded();
+ } else if (newState === WorldWind.ENDED) {
+ this._state = newState;
+ this.notifyListeners();
+ this.callGestureCallbacks();
+ this.resetIfEventsEnded();
+ }
+};
+
+// Intentionally not documented.
+GestureRecognizer.prototype.updateRecognizersWaitingForFailure = function () {
+ // Transition gestures that are waiting for this gesture to transition to Failed.
+ for (var i = 0, len = this._requiredToFailBy.length; i < len; i++) {
+ var recognizer = this._requiredToFailBy[i];
+ if (recognizer._nextState != null) {
+ recognizer.transitionToState(recognizer._nextState);
+ }
+ }
+};
+
+// Intentionally not documented.
+GestureRecognizer.prototype.tryToRecognize = function (newState) {
+ // Transition to Failed if another gesture can prevent this gesture from recognizing.
+ if (GestureRecognizer.allRecognizers.some(this.canBePreventedByRecognizer, this)) {
+ this.transitionToState(WorldWind.FAILED);
+ return;
+ }
+
+ // Delay the transition to Recognized/Began if this gesture is waiting for a gesture in the Possible state.
+ if (GestureRecognizer.allRecognizers.some(this.isWaitingForRecognizerToFail, this)) {
+ this._nextState = newState;
+ return;
+ }
+
+ // Transition to Failed all other gestures that can be prevented from recognizing by this gesture.
+ var prevented = GestureRecognizer.allRecognizers.filter(this.canPreventRecognizer, this);
+ for (var i = 0, len = prevented.length; i < len; i++) {
+ prevented[i].transitionToState(WorldWind.FAILED);
+ }
+
+ this._state = newState;
+};
+
+// Intentionally not documented.
+GestureRecognizer.prototype.canPreventRecognizer = function (that) {
+ return this != that && this.target == that.target && that.state == WorldWind.POSSIBLE &&
+ (this.requiredToFailByRecognizer(that) || !this.canRecognizeSimultaneouslyWith(that));
+};
+
+// Intentionally not documented.
+GestureRecognizer.prototype.canBePreventedByRecognizer = function (that) {
+ return this != that && this.target == that.target && that.state == WorldWind.RECOGNIZED &&
+ (this.requiresRecognizerToFail(that) || !this.canRecognizeSimultaneouslyWith(that));
+};
+
+// Intentionally not documented.
+GestureRecognizer.prototype.isWaitingForRecognizerToFail = function (that) {
+ return this != that && this.target == that.target && that.state == WorldWind.POSSIBLE &&
+ this.requiresRecognizerToFail(that);
+};
+
+/**
+ * Registers a gesture state listener on this GestureRecognizer. Registering state listeners using this function
+ * enables applications to receive notifications of gesture recognition.
+ *
+ * Listeners must implement a gestureStateChanged method to receive notifications. The gestureStateChanged method will
+ * receive one parameter containing a reference to the recognizer that changed state.
+ *
+ * @param listener The function to call when the event occurs.
+ * @throws {ArgumentError} If any argument is null or undefined.
+ */
+GestureRecognizer.prototype.addListener = function (listener) {
+ if (!listener) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "GestureRecognizer", "addListener", "missingListener"));
+ }
+ this.listenerList.push(listener);
+};
+
+/**
+ * Removes a gesture state listener from this GestureRecognizer. The listener must be the same object passed to
+ * addListener. Calling removeListener with arguments that do not identify a currently registered
+ * listener has no effect.
+ *
+ * @param listener The listener to remove. Must be the same object passed to addListener.
+ * @throws {ArgumentError} If any argument is null or undefined.
+ */
+GestureRecognizer.prototype.removeListener = function (listener) {
+ if (!listener) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "GestureRecognizer", "removeListener", "missingListener"));
+ }
+
+ var index = this.listenerList.indexOf(listener);
+ if (index !== -1) {
+ this.listenerList.splice(index, 1); // remove the listener from the list
+ }
+};
+
+// Intentionally not documented.
+GestureRecognizer.prototype.notifyListeners = function () {
+ for (var i = 0; i < this.listenerList.length; i++) {
+ this.listenerList[i].gestureStateChanged(this);
+ }
+};
+
+// Intentionally not documented.
+GestureRecognizer.prototype.callGestureCallbacks = function () {
+ for (var i = 0, len = this._gestureCallbacks.length; i < len; i++) {
+ this._gestureCallbacks[i](this);
+ }
+};
+
+// Intentionally not documented.
+GestureRecognizer.prototype.onGestureEvent = function (event) {
+ if (!this.enabled) {
+ return;
+ }
+
+ if (event.defaultPrevented && this.state === WorldWind.POSSIBLE) {
+ return; // ignore cancelled events while in the Possible state
+ }
+
+ var i, len;
+
+ try {
+ if (event.type === "mousedown") {
+ this.handleMouseDown(event);
+ } else if (event.type === "mousemove") {
+ this.handleMouseMove(event);
+ } else if (event.type === "mouseup") {
+ this.handleMouseUp(event);
+ } else if (event.type === "touchstart") {
+ for (i = 0, len = event.changedTouches.length; i < len; i++) {
+ this.handleTouchStart(event.changedTouches.item(i));
+ }
+ } else if (event.type === "touchmove") {
+ for (i = 0, len = event.changedTouches.length; i < len; i++) {
+ this.handleTouchMove(event.changedTouches.item(i));
+ }
+ } else if (event.type === "touchcancel") {
+ for (i = 0, len = event.changedTouches.length; i < len; i++) {
+ this.handleTouchCancel(event.changedTouches.item(i));
+ }
+ } else if (event.type === "touchend") {
+ for (i = 0, len = event.changedTouches.length; i < len; i++) {
+ this.handleTouchEnd(event.changedTouches.item(i));
+ }
+ } else if (event.type === "pointerdown" && event.pointerType === "mouse") {
+ this.handleMouseDown(event);
+ } else if (event.type === "pointermove" && event.pointerType === "mouse") {
+ this.handleMouseMove(event);
+ } else if (event.type === "pointercancel" && event.pointerType === "mouse") {
+ // Intentionally left blank. The W3C Pointer Events specification is ambiguous on what cancel means
+ // for mouse input, and there is no evidence that this event is actually generated (6/19/2015).
+ } else if (event.type === "pointerup" && event.pointerType === "mouse") {
+ this.handleMouseUp(event);
+ } else if (event.type === "pointerdown" && event.pointerType === "touch") {
+ this.handleTouchStart(event);
+ } else if (event.type === "pointermove" && event.pointerType === "touch") {
+ this.handleTouchMove(event);
+ } else if (event.type === "pointercancel" && event.pointerType === "touch") {
+ this.handleTouchCancel(event);
+ } else if (event.type === "pointerup" && event.pointerType === "touch") {
+ this.handleTouchEnd(event);
+ } else {
+ Logger.logMessage(Logger.LEVEL_INFO, "GestureRecognizer", "handleEvent",
+ "Unrecognized event type: " + event.type);
+ }
+ } catch (e) {
+ Logger.logMessage(Logger.LEVEL_SEVERE, "GestureRecognizer", "handleEvent",
+ "Error handling event.\n" + e.toString());
+ }
+};
+
+// Intentionally not documented.
+GestureRecognizer.prototype.handleMouseDown = function (event) {
+ if (event.type == "mousedown" && this._touches.length > 0) {
+ return; // ignore synthesized mouse down events on Android Chrome
+ }
+
+ var buttonBit = 1 << event.button;
+ if (buttonBit & this._mouseButtonMask != 0) {
+ return; // ignore redundant mouse down events
+ }
+
+ if (this._mouseButtonMask == 0) { // first button down
+ this._clientX = event.clientX;
+ this._clientY = event.clientY;
+ this._clientStartX = event.clientX;
+ this._clientStartY = event.clientY;
+ this._translationX = 0;
+ this._translationY = 0;
+ }
+
+ this._mouseButtonMask |= buttonBit;
+ this.mouseDown(event);
+};
+
+// Intentionally not documented.
+GestureRecognizer.prototype.handleMouseMove = function (event) {
+ if (this._mouseButtonMask == 0) {
+ return; // ignore mouse move events when this recognizer does not consider any button to be down
+ }
+
+ if (this._clientX == event.clientX && this._clientY == event._clientY) {
+ return; // ignore redundant mouse move events
+ }
+
+ var dx = event.clientX - this._clientStartX,
+ dy = event.clientY - this._clientStartY,
+ w = this._translationWeight;
+ this._clientX = event.clientX;
+ this._clientY = event.clientY;
+ this._translationX = this._translationX * (1 - w) + dx * w;
+ this._translationY = this._translationY * (1 - w) + dy * w;
+ this.mouseMove(event);
+};
+
+// Intentionally not documented.
+GestureRecognizer.prototype.handleMouseUp = function (event) {
+ var buttonBit = 1 << event.button;
+ if (buttonBit & this._mouseButtonMask == 0) {
+ return; // ignore mouse up events for buttons this recognizer does not consider to be down
+ }
+
+ this._mouseButtonMask &= ~buttonBit;
+ this.mouseUp(event);
+
+ if (this._mouseButtonMask == 0) {
+ this.resetIfEventsEnded(); // last button up
+ }
+};
+
+// Intentionally not documented.
+GestureRecognizer.prototype.handleTouchStart = function (event) {
+ var touch = new Touch(event.identifier || event.pointerId, event.clientX, event.clientY); // touch events or pointer events
+ this._touches.push(touch);
+
+ if (this._touches.length == 1) { // first touch
+ this._clientX = event.clientX;
+ this._clientY = event.clientY;
+ this._clientStartX = event.clientX;
+ this._clientStartY = event.clientY;
+ this._translationX = 0;
+ this._translationY = 0;
+ this._touchCentroidShiftX = 0;
+ this._touchCentroidShiftY = 0;
+ } else {
+ this.touchesAddedOrRemoved();
+ }
+
+ this.touchStart(touch);
+};
+
+// Intentionally not documented.
+GestureRecognizer.prototype.handleTouchMove = function (event) {
+ var index = this.indexOfTouchWithId(event.identifier || event.pointerId); // touch events or pointer events
+ if (index == -1) {
+ return; // ignore events for touches that did not start in this recognizer's target
+ }
+
+ var touch = this._touches[index];
+ if (touch.clientX == event.clientX && touch.clientY == event.clientY) {
+ return; // ignore redundant touch move events, which we've encountered on Android Chrome
+ }
+
+ touch.clientX = event.clientX;
+ touch.clientY = event.clientY;
+
+ var centroid = this.touchCentroid(),
+ dx = centroid.clientX - this._clientStartX + this._touchCentroidShiftX,
+ dy = centroid.clientY - this._clientStartY + this._touchCentroidShiftY,
+ w = this._translationWeight;
+ this._clientX = centroid.clientX;
+ this._clientY = centroid.clientY;
+ this._translationX = this._translationX * (1 - w) + dx * w;
+ this._translationY = this._translationY * (1 - w) + dy * w;
+
+ this.touchMove(touch);
+};
+
+// Intentionally not documented.
+GestureRecognizer.prototype.handleTouchCancel = function (event) {
+ var index = this.indexOfTouchWithId(event.identifier || event.pointerId); // touch events or pointer events
+ if (index == -1) {
+ return; // ignore events for touches that did not start in this recognizer's target
+ }
+
+ var touch = this._touches[index];
+ this._touches.splice(index, 1);
+ this.touchesAddedOrRemoved();
+ this.touchCancel(touch);
+ this.resetIfEventsEnded();
+};
+
+// Intentionally not documented.
+GestureRecognizer.prototype.handleTouchEnd = function (event) {
+ var index = this.indexOfTouchWithId(event.identifier || event.pointerId); // touch events or pointer events
+ if (index == -1) {
+ return; // ignore events for touches that did not start in this recognizer's target
+ }
+
+ var touch = this._touches[index];
+ this._touches.splice(index, 1);
+ this.touchesAddedOrRemoved();
+ this.touchEnd(touch);
+ this.resetIfEventsEnded();
+};
+
+// Intentionally not documented.
+GestureRecognizer.prototype.resetIfEventsEnded = function () {
+ if (this._state != WorldWind.POSSIBLE && this._mouseButtonMask == 0 && this._touches.length == 0) {
+ this.reset();
+ }
+};
+
+// Intentionally not documented.
+GestureRecognizer.prototype.touchesAddedOrRemoved = function () {
+ this._touchCentroidShiftX += this._clientX;
+ this._touchCentroidShiftY += this._clientY;
+ var centroid = this.touchCentroid();
+ this._clientX = centroid.clientX;
+ this._clientY = centroid.clientY;
+ this._touchCentroidShiftX -= this._clientX;
+ this._touchCentroidShiftY -= this._clientY;
+};
+
+// Intentionally not documented.
+GestureRecognizer.prototype.touchCentroid = function () {
+ var x = 0,
+ y = 0;
+
+ for (var i = 0, len = this._touches.length; i < len; i++) {
+ var touch = this._touches[i];
+ x += touch.clientX / len;
+ y += touch.clientY / len;
+ }
+
+ return { clientX: x, clientY: y };
+};
+
+// Intentionally not documented.
+GestureRecognizer.prototype.indexOfTouchWithId = function (identifier) {
+ for (var i = 0, len = this._touches.length; i < len; i++) {
+ if (this._touches[i].identifier == identifier) {
+ return i;
+ }
+ }
+
+ return -1;
+};
+
+export default GestureRecognizer;
+
diff --git a/web/test/WebWorldWind/src/gesture/PanRecognizer.js b/web/test/WebWorldWind/src/gesture/PanRecognizer.js
new file mode 100644
index 00000000..e3ee8455
--- /dev/null
+++ b/web/test/WebWorldWind/src/gesture/PanRecognizer.js
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports PanRecognizer
+ */
+import GestureRecognizer from '../gesture/GestureRecognizer';
+
+
+/**
+ * Constructs a pan gesture recognizer.
+ * @alias PanRecognizer
+ * @constructor
+ * @augments GestureRecognizer
+ * @classdesc A concrete gesture recognizer subclass that looks for touch panning gestures.
+ * @param {EventTarget} target The document element this gesture recognizer observes for mouse and touch events.
+ * @param {Function} callback An optional function to call when this gesture is recognized. If non-null, the
+ * function is called when this gesture is recognized, and is passed a single argument: this gesture recognizer,
+ * e.g., gestureCallback(recognizer).
+ * @throws {ArgumentError} If the specified target is null or undefined.
+ */
+function PanRecognizer(target, callback) {
+ GestureRecognizer.call(this, target, callback);
+
+ /**
+ *
+ * @type {Number}
+ */
+ this.minNumberOfTouches = 1;
+
+ /**
+ *
+ * @type {Number}
+ */
+ this.maxNumberOfTouches = Number.MAX_VALUE;
+
+ // Intentionally not documented.
+ this.interpretDistance = 20;
+}
+
+PanRecognizer.prototype = Object.create(GestureRecognizer.prototype);
+
+// Documented in superclass.
+PanRecognizer.prototype.mouseDown = function (event) {
+ if (this.state == WorldWind.POSSIBLE) {
+ this.state = WorldWind.FAILED; // touch gestures fail upon receiving a mouse event
+ }
+};
+
+// Documented in superclass.
+PanRecognizer.prototype.touchMove = function (touch) {
+ if (this.state == WorldWind.POSSIBLE) {
+ if (this.shouldInterpret()) {
+ if (this.shouldRecognize()) {
+ this.state = WorldWind.BEGAN;
+ } else {
+ this.state = WorldWind.FAILED;
+ }
+ }
+ } else if (this.state == WorldWind.BEGAN || this.state == WorldWind.CHANGED) {
+ this.state = WorldWind.CHANGED;
+ }
+};
+
+// Documented in superclass.
+PanRecognizer.prototype.touchEnd = function (touch) {
+ if (this.touchCount == 0) { // last touch ended
+ if (this.state == WorldWind.POSSIBLE) {
+ this.state = WorldWind.FAILED;
+ } else if (this.state == WorldWind.BEGAN || this.state == WorldWind.CHANGED) {
+ this.state = WorldWind.ENDED;
+ }
+ }
+};
+
+// Documented in superclass.
+PanRecognizer.prototype.touchCancel = function (touch) {
+ if (this.touchCount == 0) { // last touch cancelled
+ if (this.state == WorldWind.POSSIBLE) {
+ this.state = WorldWind.FAILED;
+ } else if (this.state == WorldWind.BEGAN || this.state == WorldWind.CHANGED) {
+ this.state = WorldWind.CANCELLED;
+ }
+ }
+};
+
+// Documented in superclass.
+PanRecognizer.prototype.prepareToRecognize = function () {
+ // set translation to zero when the pan begins
+ this.translationX = 0;
+ this.translationY = 0;
+};
+
+/**
+ *
+ * @returns {boolean}
+ * @protected
+ */
+PanRecognizer.prototype.shouldInterpret = function () {
+ var dx = this.translationX,
+ dy = this.translationY,
+ distance = Math.sqrt(dx * dx + dy * dy);
+ return distance > this.interpretDistance; // interpret touches when the touch centroid moves far enough
+};
+
+/**
+ *
+ * @returns {boolean}
+ * @protected
+ */
+PanRecognizer.prototype.shouldRecognize = function () {
+ var touchCount = this.touchCount;
+ return touchCount != 0
+ && touchCount >= this.minNumberOfTouches
+ && touchCount <= this.maxNumberOfTouches;
+};
+
+export default PanRecognizer;
+
diff --git a/web/test/WebWorldWind/src/gesture/PinchRecognizer.js b/web/test/WebWorldWind/src/gesture/PinchRecognizer.js
new file mode 100644
index 00000000..346707b9
--- /dev/null
+++ b/web/test/WebWorldWind/src/gesture/PinchRecognizer.js
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports PinchRecognizer
+ */
+import GestureRecognizer from '../gesture/GestureRecognizer';
+
+
+/**
+ * Constructs a pinch gesture recognizer.
+ * @alias PinchRecognizer
+ * @constructor
+ * @augments GestureRecognizer
+ * @classdesc A concrete gesture recognizer subclass that looks for two finger pinch gestures.
+ * @param {EventTarget} target The document element this gesture recognizer observes for mouse and touch events.
+ * @param {Function} callback An optional function to call when this gesture is recognized. If non-null, the
+ * function is called when this gesture is recognized, and is passed a single argument: this gesture recognizer,
+ * e.g., gestureCallback(recognizer).
+ * @throws {ArgumentError} If the specified target is null or undefined.
+ */
+function PinchRecognizer(target, callback) {
+ GestureRecognizer.call(this, target, callback);
+
+ // Intentionally not documented.
+ this._scale = 1;
+
+ // Intentionally not documented.
+ this._offsetScale = 1;
+
+ // Intentionally not documented.
+ this.referenceDistance = 0;
+
+ // Intentionally not documented.
+ this.interpretThreshold = 20;
+
+ // Intentionally not documented.
+ this.weight = 0.4;
+
+ // Intentionally not documented.
+ this.pinchTouches = [];
+}
+
+PinchRecognizer.prototype = Object.create(GestureRecognizer.prototype);
+
+Object.defineProperties(PinchRecognizer.prototype, {
+ scale: {
+ get: function () {
+ return this._scale * this._offsetScale;
+ }
+ }
+});
+
+// Documented in superclass.
+PinchRecognizer.prototype.reset = function () {
+ GestureRecognizer.prototype.reset.call(this);
+
+ this._scale = 1;
+ this._offsetScale = 1;
+ this.referenceDistance = 0;
+ this.pinchTouches = [];
+};
+
+// Documented in superclass.
+PinchRecognizer.prototype.mouseDown = function (event) {
+ if (this.state == WorldWind.POSSIBLE) {
+ this.state = WorldWind.FAILED; // touch gestures fail upon receiving a mouse event
+ }
+};
+
+// Documented in superclass.
+PinchRecognizer.prototype.touchStart = function (touch) {
+ if (this.pinchTouches.length < 2) {
+ if (this.pinchTouches.push(touch) == 2) {
+ this.referenceDistance = this.currentPinchDistance();
+ this._offsetScale *= this._scale;
+ this._scale = 1;
+ }
+ }
+};
+
+// Documented in superclass.
+PinchRecognizer.prototype.touchMove = function (touch) {
+ if (this.pinchTouches.length == 2) {
+ if (this.state == WorldWind.POSSIBLE) {
+ if (this.shouldRecognize()) {
+ this.state = WorldWind.BEGAN;
+ }
+ } else if (this.state == WorldWind.BEGAN || this.state == WorldWind.CHANGED) {
+ var distance = this.currentPinchDistance(),
+ newScale = Math.abs(distance / this.referenceDistance),
+ w = this.weight;
+ this._scale = this._scale * (1 - w) + newScale * w;
+ this.state = WorldWind.CHANGED;
+ }
+ }
+};
+
+// Documented in superclass.
+PinchRecognizer.prototype.touchEnd = function (touch) {
+ var index = this.pinchTouches.indexOf(touch);
+ if (index != -1) {
+ this.pinchTouches.splice(index, 1);
+ }
+
+ // Transition to the ended state if this was the last touch.
+ if (this.touchCount == 0) { // last touch ended
+ if (this.state == WorldWind.POSSIBLE) {
+ this.state = WorldWind.FAILED;
+ } else if (this.state == WorldWind.BEGAN || this.state == WorldWind.CHANGED) {
+ this.state = WorldWind.ENDED;
+ }
+ }
+};
+
+// Documented in superclass.
+PinchRecognizer.prototype.touchCancel = function (touch) {
+ var index = this.pinchTouches.indexOf(touch);
+ if (index != -1) {
+ this.pinchTouches.splice(index, 1);
+ }
+
+ // Transition to the cancelled state if this was the last touch.
+ if (this.touchCount == 0) {
+ if (this.state == WorldWind.POSSIBLE) {
+ this.state = WorldWind.FAILED;
+ } else if (this.state == WorldWind.BEGAN || this.state == WorldWind.CHANGED) {
+ this.state = WorldWind.CANCELLED;
+ }
+ }
+};
+
+// Documented in superclass.
+PinchRecognizer.prototype.prepareToRecognize = function () {
+ this.referenceDistance = this.currentPinchDistance();
+ this._scale = 1;
+};
+
+// Intentionally not documented.
+PinchRecognizer.prototype.shouldRecognize = function () {
+ var distance = this.currentPinchDistance();
+
+ return Math.abs(distance - this.referenceDistance) > this.interpretThreshold;
+};
+
+// Intentionally not documented.
+PinchRecognizer.prototype.currentPinchDistance = function () {
+ var touch0 = this.pinchTouches[0],
+ touch1 = this.pinchTouches[1],
+ dx = touch0.clientX - touch1.clientX,
+ dy = touch0.clientY - touch1.clientY;
+
+ return Math.sqrt(dx * dx + dy * dy);
+};
+
+export default PinchRecognizer;
diff --git a/web/test/WebWorldWind/src/gesture/RotationRecognizer.js b/web/test/WebWorldWind/src/gesture/RotationRecognizer.js
new file mode 100644
index 00000000..6417fff3
--- /dev/null
+++ b/web/test/WebWorldWind/src/gesture/RotationRecognizer.js
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports RotationRecognizer
+ */
+import Angle from '../geom/Angle';
+import GestureRecognizer from '../gesture/GestureRecognizer';
+
+
+/**
+ * Constructs a rotation gesture recognizer.
+ * @alias RotationRecognizer
+ * @constructor
+ * @augments GestureRecognizer
+ * @classdesc A concrete gesture recognizer subclass that looks for two finger rotation gestures.
+ * @param {EventTarget} target The document element this gesture recognizer observes for mouse and touch events.
+ * @param {Function} callback An optional function to call when this gesture is recognized. If non-null, the
+ * function is called when this gesture is recognized, and is passed a single argument: this gesture recognizer,
+ * e.g., gestureCallback(recognizer).
+ * @throws {ArgumentError} If the specified target is null or undefined.
+ */
+function RotationRecognizer(target, callback) {
+ GestureRecognizer.call(this, target, callback);
+
+ // Intentionally not documented.
+ this._rotation = 0;
+
+ // Intentionally not documented.
+ this._offsetRotation = 0;
+
+ // Intentionally not documented.
+ this.referenceAngle = 0;
+
+ // Intentionally not documented.
+ this.interpretThreshold = 20;
+
+ // Intentionally not documented.
+ this.weight = 0.4;
+
+ // Intentionally not documented.
+ this.rotationTouches = [];
+}
+
+RotationRecognizer.prototype = Object.create(GestureRecognizer.prototype);
+
+Object.defineProperties(RotationRecognizer.prototype, {
+ rotation: {
+ get: function () {
+ return this._rotation + this._offsetRotation;
+ }
+ }
+});
+
+// Documented in superclass.
+RotationRecognizer.prototype.reset = function () {
+ GestureRecognizer.prototype.reset.call(this);
+
+ this._rotation = 0;
+ this._offsetRotation = 0;
+ this.referenceAngle = 0;
+ this.rotationTouches = [];
+};
+
+// Documented in superclass.
+RotationRecognizer.prototype.mouseDown = function (event) {
+ if (this.state == WorldWind.POSSIBLE) {
+ this.state = WorldWind.FAILED; // touch gestures fail upon receiving a mouse event
+ }
+};
+
+// Documented in superclass.
+RotationRecognizer.prototype.touchStart = function (touch) {
+ if (this.rotationTouches.length < 2) {
+ if (this.rotationTouches.push(touch) == 2) {
+ this.referenceAngle = this.currentTouchAngle();
+ this._offsetRotation += this._rotation;
+ this._rotation = 0;
+ }
+ }
+};
+
+// Documented in superclass.
+RotationRecognizer.prototype.touchMove = function (touch) {
+ if (this.rotationTouches.length == 2) {
+ if (this.state == WorldWind.POSSIBLE) {
+ if (this.shouldRecognize()) {
+ this.state = WorldWind.BEGAN;
+ }
+ } else if (this.state == WorldWind.BEGAN || this.state == WorldWind.CHANGED) {
+ var angle = this.currentTouchAngle(),
+ newRotation = Angle.normalizedDegrees(angle - this.referenceAngle),
+ w = this.weight;
+ this._rotation = this._rotation * (1 - w) + newRotation * w;
+ this.state = WorldWind.CHANGED;
+ }
+ }
+};
+
+// Documented in superclass.
+RotationRecognizer.prototype.touchEnd = function (touch) {
+ var index = this.rotationTouches.indexOf(touch);
+ if (index != -1) {
+ this.rotationTouches.splice(index, 1);
+ }
+
+ // Transition to the ended state if this was the last touch.
+ if (this.touchCount == 0) { // last touch ended
+ if (this.state == WorldWind.POSSIBLE) {
+ this.state = WorldWind.FAILED;
+ } else if (this.state == WorldWind.BEGAN || this.state == WorldWind.CHANGED) {
+ this.state = WorldWind.ENDED;
+ }
+ }
+};
+
+// Documented in superclass.
+RotationRecognizer.prototype.touchCancel = function (touch) {
+ var index = this.rotationTouches.indexOf(touch);
+ if (index != -1) {
+ this.rotationTouches.splice(index, 1);
+
+ // Transition to the cancelled state if this was the last touch.
+ if (this.touchCount == 0) {
+ if (this.state == WorldWind.POSSIBLE) {
+ this.state = WorldWind.FAILED;
+ } else if (this.state == WorldWind.BEGAN || this.state == WorldWind.CHANGED) {
+ this.state = WorldWind.CANCELLED;
+ }
+ }
+ }
+};
+
+// Documented in superclass.
+RotationRecognizer.prototype.prepareToRecognize = function () {
+ this.referenceAngle = this.currentTouchAngle();
+ this._rotation = 0;
+};
+
+// Intentionally not documented.
+RotationRecognizer.prototype.shouldRecognize = function () {
+ var angle = this.currentTouchAngle(),
+ rotation = Angle.normalizedDegrees(angle - this.referenceAngle);
+
+ return Math.abs(rotation) > this.interpretThreshold;
+};
+
+// Intentionally not documented.
+RotationRecognizer.prototype.currentTouchAngle = function () {
+ var touch0 = this.rotationTouches[0],
+ touch1 = this.rotationTouches[1],
+ dx = touch0.clientX - touch1.clientX,
+ dy = touch0.clientY - touch1.clientY;
+
+ return Math.atan2(dy, dx) * Angle.RADIANS_TO_DEGREES;
+};
+
+export default RotationRecognizer;
+
diff --git a/web/test/WebWorldWind/src/gesture/TapRecognizer.js b/web/test/WebWorldWind/src/gesture/TapRecognizer.js
new file mode 100644
index 00000000..d01c2979
--- /dev/null
+++ b/web/test/WebWorldWind/src/gesture/TapRecognizer.js
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports TapRecognizer
+ */
+import GestureRecognizer from '../gesture/GestureRecognizer';
+
+
+/**
+ * Constructs a tap gesture recognizer.
+ * @alias TapRecognizer
+ * @constructor
+ * @augments GestureRecognizer
+ * @classdesc A concrete gesture recognizer subclass that looks for single or multiple taps.
+ * @param {EventTarget} target The document element this gesture recognizer observes for mouse and touch events.
+ * @param {Function} callback An optional function to call when this gesture is recognized. If non-null, the
+ * function is called when this gesture is recognized, and is passed a single argument: this gesture recognizer,
+ * e.g., gestureCallback(recognizer).
+ * @throws {ArgumentError} If the specified target is null or undefined.
+ */
+function TapRecognizer(target, callback) {
+ GestureRecognizer.call(this, target, callback);
+
+ /**
+ *
+ * @type {Number}
+ */
+ this.numberOfTaps = 1;
+
+ /**
+ *
+ * @type {Number}
+ */
+ this.numberOfTouches = 1;
+
+ // Intentionally not documented.
+ this.maxTouchMovement = 20;
+
+ // Intentionally not documented.
+ this.maxTapDuration = 500;
+
+ // Intentionally not documented.
+ this.maxTapInterval = 400;
+
+ // Intentionally not documented.
+ this.taps = [];
+
+ // Intentionally not documented.
+ this.timeout = null;
+}
+
+TapRecognizer.prototype = Object.create(GestureRecognizer.prototype);
+
+// Documented in superclass.
+TapRecognizer.prototype.reset = function () {
+ GestureRecognizer.prototype.reset.call(this);
+
+ this.taps = [];
+ this.cancelFailAfterDelay();
+};
+
+// Documented in superclass.
+TapRecognizer.prototype.mouseDown = function (event) {
+ if (this.state != WorldWind.POSSIBLE) {
+ return;
+ }
+
+ this.state = WorldWind.FAILED; // touch gestures fail upon receiving a mouse event
+};
+
+// Documented in superclass.
+TapRecognizer.prototype.touchStart = function (touch) {
+ if (this.state != WorldWind.POSSIBLE) {
+ return;
+ }
+
+ var tap;
+
+ if (this.touchCount > this.numberOfTouches) {
+ this.state = WorldWind.FAILED;
+ } else if (this.touchCount == 1) { // first touch started
+ tap = {
+ touchCount: this.touchCount,
+ clientX: this.clientX,
+ clientY: this.clientY
+ };
+ this.taps.push(tap);
+ this.failAfterDelay(this.maxTapDuration); // fail if the tap is down too long
+ } else {
+ tap = this.taps[this.taps.length - 1];
+ tap.touchCount = this.touchCount; // max number of simultaneous touches
+ tap.clientX = this.clientX; // touch centroid
+ tap.clientY = this.clientY;
+ }
+};
+
+// Documented in superclass.
+TapRecognizer.prototype.touchMove = function (touch) {
+ if (this.state != WorldWind.POSSIBLE) {
+ return;
+ }
+
+ var dx = this.translationX,
+ dy = this.translationY,
+ distance = Math.sqrt(dx * dx + dy * dy);
+ if (distance > this.maxTouchMovement) {
+ this.state = WorldWind.FAILED;
+ }
+};
+
+// Documented in superclass.
+TapRecognizer.prototype.touchEnd = function (touch) {
+ if (this.state != WorldWind.POSSIBLE) {
+ return;
+ }
+
+ if (this.touchCount != 0) {
+ return; // wait until the last touch ends
+ }
+
+ var tapCount = this.taps.length,
+ tap = this.taps[tapCount - 1];
+ if (tap.touchCount != this.numberOfTouches) {
+ this.state = WorldWind.FAILED; // wrong number of touches
+ } else if (tapCount == this.numberOfTaps) {
+ this.clientX = this.taps[0].clientX;
+ this.clientY = this.taps[0].clientY;
+ this.state = WorldWind.RECOGNIZED;
+ } else {
+ this.failAfterDelay(this.maxTapInterval); // fail if the interval between taps is too long
+ }
+};
+
+// Documented in superclass.
+TapRecognizer.prototype.touchCancel = function (touch) {
+ if (this.state != WorldWind.POSSIBLE) {
+ return;
+ }
+
+ this.state = WorldWind.FAILED;
+};
+
+// Intentionally not documented.
+TapRecognizer.prototype.failAfterDelay = function (delay) {
+ var self = this;
+ if (self.timeout) {
+ window.clearTimeout(self.timeout);
+ }
+
+ self.timeout = window.setTimeout(function () {
+ self.timeout = null;
+ if (self.state == WorldWind.POSSIBLE) {
+ self.state = WorldWind.FAILED; // fail if we haven't already reached a terminal state
+ }
+ }, delay);
+};
+
+// Intentionally not documented.
+TapRecognizer.prototype.cancelFailAfterDelay = function () {
+ var self = this;
+ if (self.timeout) {
+ window.clearTimeout(self.timeout);
+ self.timeout = null;
+ }
+};
+
+export default TapRecognizer;
+
diff --git a/web/test/WebWorldWind/src/gesture/TiltRecognizer.js b/web/test/WebWorldWind/src/gesture/TiltRecognizer.js
new file mode 100644
index 00000000..8ff01aff
--- /dev/null
+++ b/web/test/WebWorldWind/src/gesture/TiltRecognizer.js
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports TiltRecognizer
+ */
+import PanRecognizer from '../gesture/PanRecognizer';
+
+
+/**
+ * Constructs a tilt gesture recognizer.
+ * @alias TiltRecognizer
+ * @constructor
+ * @augments PanRecognizer
+ * @classdesc A concrete gesture recognizer subclass that looks for two finger tilt gestures.
+ * @param {EventTarget} target The document element this gesture recognizer observes for mouse and touch events.
+ * @param {Function} callback An optional function to call when this gesture is recognized. If non-null, the
+ * function is called when this gesture is recognized, and is passed a single argument: this gesture recognizer,
+ * e.g., gestureCallback(recognizer).
+ * @throws {ArgumentError} If the specified target is null or undefined.
+ */
+function TiltRecognizer(target, callback) {
+ PanRecognizer.call(this, target, callback);
+
+ // Intentionally not documented.
+ this.maxTouchDistance = 250;
+
+ // Intentionally not documented.
+ this.maxTouchDivergence = 50;
+}
+
+// Intentionally not documented.
+TiltRecognizer.LEFT = 1 << 0;
+
+// Intentionally not documented.
+TiltRecognizer.RIGHT = 1 << 1;
+
+// Intentionally not documented.
+TiltRecognizer.UP = 1 << 2;
+
+// Intentionally not documented.
+TiltRecognizer.DOWN = 1 << 3;
+
+TiltRecognizer.prototype = Object.create(PanRecognizer.prototype);
+
+// Documented in superclass.
+TiltRecognizer.prototype.shouldInterpret = function () {
+ for (var i = 0, count = this.touchCount; i < count; i++) {
+ var touch = this.touch(i),
+ dx = touch.translationX,
+ dy = touch.translationY,
+ distance = Math.sqrt(dx * dx + dy * dy);
+ if (distance > this.interpretDistance) {
+ return true; // interpret touches when any touch moves far enough
+ }
+ }
+
+ return false;
+};
+
+// Documented in superclass.
+TiltRecognizer.prototype.shouldRecognize = function () {
+ var touchCount = this.touchCount;
+ if (touchCount < 2) {
+ return false;
+ }
+
+ var touch0 = this.touch(0),
+ touch1 = this.touch(1),
+ dx = touch0.clientX - touch1.clientX,
+ dy = touch0.clientY - touch1.clientY,
+ distance = Math.sqrt(dx * dx + dy * dy);
+ if (distance > this.maxTouchDistance) {
+ return false; // touches must be close together
+ }
+
+ var tx = touch0.translationX - touch1.translationX,
+ ty = touch0.translationY - touch1.translationY,
+ divergence = Math.sqrt(tx * tx + ty * ty);
+ if (divergence > this.maxTouchDivergence) {
+ return false; // touches must be moving in a mostly parallel direction
+ }
+
+ var verticalMask = TiltRecognizer.UP | TiltRecognizer.DOWN,
+ dirMask0 = this.touchDirection(touch0) & verticalMask,
+ dirMask1 = this.touchDirection(touch1) & verticalMask;
+ return (dirMask0 & dirMask1) != 0; // touches must move in the same vertical direction
+};
+
+// Intentionally not documented.
+TiltRecognizer.prototype.touchDirection = function (touch) {
+ var dx = touch.translationX,
+ dy = touch.translationY,
+ dirMask = 0;
+
+ if (Math.abs(dx) > Math.abs(dy)) {
+ dirMask |= dx < 0 ? TiltRecognizer.LEFT : 0;
+ dirMask |= dx > 0 ? TiltRecognizer.RIGHT : 0;
+ } else {
+ dirMask |= dy < 0 ? TiltRecognizer.UP : 0;
+ dirMask |= dy > 0 ? TiltRecognizer.DOWN : 0;
+ }
+
+ return dirMask;
+};
+
+export default TiltRecognizer;
diff --git a/web/test/WebWorldWind/src/gesture/Touch.js b/web/test/WebWorldWind/src/gesture/Touch.js
new file mode 100644
index 00000000..572d7084
--- /dev/null
+++ b/web/test/WebWorldWind/src/gesture/Touch.js
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports Touch
+ */
+
+
+
+/**
+ * Constructs a touch point.
+ * @alias Touch
+ * @constructor
+ * @classdesc Represents a touch point.
+ * @param {Color} identifier A number uniquely identifying the touch point
+ * @param {Number} clientX The X coordinate of the touch point's location.
+ * @param {Number} clientY The Y coordinate of the touch point's location.
+ */
+function Touch(identifier, clientX, clientY) {
+
+ /**
+ * A number uniquely identifying this touch point.
+ * @type {Number}
+ * @readonly
+ */
+ this.identifier = identifier;
+
+ // Intentionally not documented.
+ this._clientX = clientX;
+
+ // Intentionally not documented.
+ this._clientY = clientY;
+
+ // Intentionally not documented.
+ this._clientStartX = clientX;
+
+ // Intentionally not documented.
+ this._clientStartY = clientY;
+}
+
+Object.defineProperties(Touch.prototype, {
+ /**
+ * Indicates the X coordinate of this touch point's location.
+ * @type {Number}
+ * @memberof Touch.prototype
+ */
+ clientX: {
+ get: function () {
+ return this._clientX;
+ },
+ set: function (value) {
+ this._clientX = value;
+ }
+ },
+
+ /**
+ * Indicates the Y coordinate of this touch point's location.
+ * @type {Number}
+ * @memberof Touch.prototype
+ */
+ clientY: {
+ get: function () {
+ return this._clientY;
+ },
+ set: function (value) {
+ this._clientY = value;
+ }
+ },
+
+ /**
+ * Indicates this touch point's translation along the X axis since the touch started.
+ * @type {Number}
+ * @memberof Touch.prototype
+ */
+ translationX: {
+ get: function () {
+ return this._clientX - this._clientStartX;
+ },
+ set: function (value) {
+ this._clientStartX = this._clientX - value;
+ }
+ },
+
+ /**
+ * Indicates this touch point's translation along the Y axis since the touch started.
+ * @type {Number}
+ * @memberof Touch.prototype
+ */
+ translationY: {
+ get: function () {
+ return this._clientY - this._clientStartY;
+ },
+ set: function (value) {
+ this._clientStartY = this._clientY - value;
+ }
+ }
+});
+
+export default Touch;
diff --git a/web/test/WebWorldWind/src/globe/ArcgisElevationCoverage.js b/web/test/WebWorldWind/src/globe/ArcgisElevationCoverage.js
new file mode 100644
index 00000000..4e1cfbda
--- /dev/null
+++ b/web/test/WebWorldWind/src/globe/ArcgisElevationCoverage.js
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports ArcgisElevationCoverage
+ */
+import Sector from '../geom/Sector';
+import TiledElevationCoverage from '../globe/TiledElevationCoverage';
+import WmsUrlBuilder from '../util/WmsUrlBuilder';
+import ElevationImage from './ElevationImage';
+import Logger from '../util/Logger';
+import WWMath from '../util/WWMath';
+import ArcgisElevationWorker from 'worker!./ArcgisElevationWorker.js';
+
+/**
+ * Constructs an Earth elevation coverage using Arcgis data.
+ * @alias ArcgisElevationCoverage
+ * @constructor
+ * @augments TiledElevationCoverage
+ * @classdesc Provides elevations for Earth. Elevations are drawn from the NASA WorldWind elevation service.
+ */
+function ArcgisElevationCoverage() {
+ // see: http://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer
+ TiledElevationCoverage.call(this, {
+ coverageSector: new Sector(-WWMath.MAX_LAT, WWMath.MAX_LAT, -180, 180),
+ resolution: 360 / 2 ** 16 / 256,
+ retrievalImageFormat: "application/bil16",
+ minElevation: -450,
+ maxElevation: 8700,
+ urlBuilder: new WmsUrlBuilder("http://localhost:2020/api/Map/Elev", "WorldElevation3D", "", "1.3.0")
+ });
+
+ this.displayName = "Arcgis Elevation Coverage";
+
+ this.worker = new ArcgisElevationWorker();
+ this.worker.onmessage = this.handleMessage.bind(this);
+}
+
+ArcgisElevationCoverage.prototype = Object.create(TiledElevationCoverage.prototype);
+
+ArcgisElevationCoverage.prototype.retrieveTileImage = function (tile) {
+ if (this.currentRetrievals.indexOf(tile.tileKey) < 0) {
+ if (this.currentRetrievals.length > this.retrievalQueueSize) {
+ return;
+ }
+
+ this.worker.postMessage({
+ tileKey: tile.tileKey,
+ x: tile.column,
+ y: tile.row,
+ z: tile.level.levelNumber
+ });
+
+ this.currentRetrievals.push(tile.tileKey);
+ }
+};
+
+ArcgisElevationCoverage.prototype.handleMessage = function (evt) {
+ let { result, tileKey, url, data, msg } = evt.data;
+ this.removeFromCurrentRetrievals(tileKey);
+ let tile = this.tileCache.entryForKey(tileKey);
+ if (!tile) {
+ // tile has been released
+ return;
+ }
+ if (result === 'success') {
+ Logger.log(Logger.LEVEL_INFO, "Elevations retrieval succeeded: " + url);
+ this.loadElevationImage(tile, data);
+ this.absentResourceList.unmarkResourceAbsent(tileKey);
+
+ // Send an event to request a redraw.
+ var e = document.createEvent('Event');
+ e.initEvent(WorldWind.REDRAW_EVENT_TYPE, true, true);
+ window.dispatchEvent(e);
+ } else if (result === 'fail') {
+ this.absentResourceList.markResourceAbsent(tileKey);
+ Logger.log(Logger.LEVEL_WARNING,
+ "Elevations retrieval failed (" + msg + "): " + url);
+ } else if (result === 'error') {
+ this.absentResourceList.markResourceAbsent(tileKey);
+ Logger.log(Logger.LEVEL_WARNING, "Elevations retrieval failed: " + url);
+ } else if (result === 'timeout') {
+ this.absentResourceList.markResourceAbsent(tileKey);
+ Logger.log(Logger.LEVEL_WARNING, "Elevations retrieval timed out: " + url);
+ }
+};
+
+// Intentionally not documented.
+ArcgisElevationCoverage.prototype.loadElevationImage = function (tile, data) {
+ var elevationImage = new ElevationImage(tile.sector, data.width, data.height);
+
+ elevationImage.imageData = data.pixelData;
+ elevationImage.size = elevationImage.imageData.length * 4;
+
+ if (elevationImage.imageData) {
+ elevationImage.findMinAndMaxElevation();
+ this.imageCache.putEntry(tile.tileKey, elevationImage, elevationImage.size);
+ this.timestamp = Date.now();
+ }
+};
+
+export default ArcgisElevationCoverage;
diff --git a/web/test/WebWorldWind/src/globe/ArcgisElevationWorker.js b/web/test/WebWorldWind/src/globe/ArcgisElevationWorker.js
new file mode 100644
index 00000000..fdfd882b
--- /dev/null
+++ b/web/test/WebWorldWind/src/globe/ArcgisElevationWorker.js
@@ -0,0 +1,2219 @@
+/* jshint forin: false, bitwise: false */
+/*
+Copyright 2015-2019 Esri
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+A copy of the license and additional notices are located with the
+source distribution at:
+
+http://github.com/Esri/lerc/
+
+Contributors: Johannes Schmid, (LERC v1)
+ Chayanika Khatua, (LERC v1)
+ Wenxue Ju (LERC v1, v2.x)
+*/
+
+/* Copyright 2015-2019 Esri. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 @preserve */
+
+/**
+ * a module for decoding LERC blobs
+ * @module Lerc
+ */
+//the original LercDecode for Version 1
+var LercDecode = (function () {
+
+ // WARNING: This decoder version can only read old version 1 Lerc blobs. Use with caution.
+
+ // Note: currently, this module only has an implementation for decoding LERC data, not encoding. The name of
+ // the class was chosen to be future proof.
+
+ var CntZImage = {};
+
+ CntZImage.defaultNoDataValue = -3.4027999387901484e+38; // smallest Float32 value
+
+ /**
+ * Decode a LERC byte stream and return an object containing the pixel data and some required and optional
+ * information about it, such as the image's width and height.
+ *
+ * @param {ArrayBuffer} input The LERC input byte stream
+ * @param {object} [options] Decoding options, containing any of the following properties:
+ * @config {number} [inputOffset = 0]
+ * Skip the first inputOffset bytes of the input byte stream. A valid LERC file is expected at that position.
+ * @config {Uint8Array} [encodedMask = null]
+ * If specified, the decoder will not read mask information from the input and use the specified encoded
+ * mask data instead. Mask header/data must not be present in the LERC byte stream in this case.
+ * @config {number} [noDataValue = LercCode.defaultNoDataValue]
+ * Pixel value to use for masked pixels.
+ * @config {ArrayBufferView|Array} [pixelType = Float32Array]
+ * The desired type of the pixelData array in the return value. Note that it is the caller's responsibility to
+ * provide an appropriate noDataValue if the default pixelType is overridden.
+ * @config {boolean} [returnMask = false]
+ * If true, the return value will contain a maskData property of type Uint8Array which has one element per
+ * pixel, the value of which is 1 or 0 depending on whether that pixel's data is present or masked. If the
+ * input LERC data does not contain a mask, maskData will not be returned.
+ * @config {boolean} [returnEncodedMask = false]
+ * If true, the return value will contain a encodedMaskData property, which can be passed into encode() as
+ * encodedMask.
+ * @config {boolean} [returnFileInfo = false]
+ * If true, the return value will have a fileInfo property that contains metadata obtained from the
+ * LERC headers and the decoding process.
+ * @config {boolean} [computeUsedBitDepths = false]
+ * If true, the fileInfo property in the return value will contain the set of all block bit depths
+ * encountered during decoding. Will only have an effect if returnFileInfo option is true.
+ * @returns {{width, height, pixelData, minValue, maxValue, noDataValue, maskData, encodedMaskData, fileInfo}}
+ */
+ CntZImage.decode = function (input, options) {
+ options = options || {};
+
+ var skipMask = options.encodedMaskData || options.encodedMaskData === null;
+ var parsedData = parse(input, options.inputOffset || 0, skipMask);
+
+ var noDataValue = options.noDataValue !== null ? options.noDataValue : CntZImage.defaultNoDataValue;
+
+ var uncompressedData = uncompressPixelValues(parsedData, options.pixelType || Float32Array,
+ options.encodedMaskData, noDataValue, options.returnMask);
+
+ var result = {
+ width: parsedData.width,
+ height: parsedData.height,
+ pixelData: uncompressedData.resultPixels,
+ minValue: uncompressedData.minValue,
+ maxValue: parsedData.pixels.maxValue,
+ noDataValue: noDataValue
+ };
+
+ if (uncompressedData.resultMask) {
+ result.maskData = uncompressedData.resultMask;
+ }
+
+ if (options.returnEncodedMask && parsedData.mask) {
+ result.encodedMaskData = parsedData.mask.bitset ? parsedData.mask.bitset : null;
+ }
+
+ if (options.returnFileInfo) {
+ result.fileInfo = formatFileInfo(parsedData);
+ if (options.computeUsedBitDepths) {
+ result.fileInfo.bitDepths = computeUsedBitDepths(parsedData);
+ }
+ }
+
+ return result;
+ };
+
+ var uncompressPixelValues = function (data, TypedArrayClass, maskBitset, noDataValue, storeDecodedMask) {
+ var blockIdx = 0;
+ var numX = data.pixels.numBlocksX;
+ var numY = data.pixels.numBlocksY;
+ var blockWidth = Math.floor(data.width / numX);
+ var blockHeight = Math.floor(data.height / numY);
+ var scale = 2 * data.maxZError;
+ var minValue = Number.MAX_VALUE, currentValue;
+ maskBitset = maskBitset || (data.mask ? data.mask.bitset : null);
+
+ var resultPixels, resultMask;
+ resultPixels = new TypedArrayClass(data.width * data.height);
+ if (storeDecodedMask && maskBitset) {
+ resultMask = new Uint8Array(data.width * data.height);
+ }
+ var blockDataBuffer = new Float32Array(blockWidth * blockHeight);
+
+ var xx, yy;
+ for (var y = 0; y <= numY; y++) {
+ var thisBlockHeight = y !== numY ? blockHeight : data.height % numY;
+ if (thisBlockHeight === 0) {
+ continue;
+ }
+ for (var x = 0; x <= numX; x++) {
+ var thisBlockWidth = x !== numX ? blockWidth : data.width % numX;
+ if (thisBlockWidth === 0) {
+ continue;
+ }
+
+ var outPtr = y * data.width * blockHeight + x * blockWidth;
+ var outStride = data.width - thisBlockWidth;
+
+ var block = data.pixels.blocks[blockIdx];
+
+ var blockData, blockPtr, constValue;
+ if (block.encoding < 2) {
+ // block is either uncompressed or bit-stuffed (encodings 0 and 1)
+ if (block.encoding === 0) {
+ // block is uncompressed
+ blockData = block.rawData;
+ } else {
+ // block is bit-stuffed
+ unstuff(block.stuffedData, block.bitsPerPixel, block.numValidPixels, block.offset, scale, blockDataBuffer, data.pixels.maxValue);
+ blockData = blockDataBuffer;
+ }
+ blockPtr = 0;
+ }
+ else if (block.encoding === 2) {
+ // block is all 0
+ constValue = 0;
+ }
+ else {
+ // block has constant value (encoding === 3)
+ constValue = block.offset;
+ }
+
+ var maskByte;
+ if (maskBitset) {
+ for (yy = 0; yy < thisBlockHeight; yy++) {
+ if (outPtr & 7) {
+ //
+ maskByte = maskBitset[outPtr >> 3];
+ maskByte <<= outPtr & 7;
+ }
+ for (xx = 0; xx < thisBlockWidth; xx++) {
+ if (!(outPtr & 7)) {
+ // read next byte from mask
+ maskByte = maskBitset[outPtr >> 3];
+ }
+ if (maskByte & 128) {
+ // pixel data present
+ if (resultMask) {
+ resultMask[outPtr] = 1;
+ }
+ currentValue = block.encoding < 2 ? blockData[blockPtr++] : constValue;
+ minValue = minValue > currentValue ? currentValue : minValue;
+ resultPixels[outPtr++] = currentValue;
+ } else {
+ // pixel data not present
+ if (resultMask) {
+ resultMask[outPtr] = 0;
+ }
+ resultPixels[outPtr++] = noDataValue;
+ }
+ maskByte <<= 1;
+ }
+ outPtr += outStride;
+ }
+ } else {
+ // mask not present, simply copy block over
+ if (block.encoding < 2) {
+ // duplicating this code block for performance reasons
+ // blockData case:
+ for (yy = 0; yy < thisBlockHeight; yy++) {
+ for (xx = 0; xx < thisBlockWidth; xx++) {
+ currentValue = blockData[blockPtr++];
+ minValue = minValue > currentValue ? currentValue : minValue;
+ resultPixels[outPtr++] = currentValue;
+ }
+ outPtr += outStride;
+ }
+ }
+ else {
+ // constValue case:
+ minValue = minValue > constValue ? constValue : minValue;
+ for (yy = 0; yy < thisBlockHeight; yy++) {
+ for (xx = 0; xx < thisBlockWidth; xx++) {
+ resultPixels[outPtr++] = constValue;
+ }
+ outPtr += outStride;
+ }
+ }
+ }
+ if (block.encoding === 1 && blockPtr !== block.numValidPixels) {
+ throw "Block and Mask do not match";
+ }
+ blockIdx++;
+ }
+ }
+
+ return {
+ resultPixels: resultPixels,
+ resultMask: resultMask,
+ minValue: minValue
+ };
+ };
+
+ var formatFileInfo = function (data) {
+ return {
+ "fileIdentifierString": data.fileIdentifierString,
+ "fileVersion": data.fileVersion,
+ "imageType": data.imageType,
+ "height": data.height,
+ "width": data.width,
+ "maxZError": data.maxZError,
+ "eofOffset": data.eofOffset,
+ "mask": data.mask ? {
+ "numBlocksX": data.mask.numBlocksX,
+ "numBlocksY": data.mask.numBlocksY,
+ "numBytes": data.mask.numBytes,
+ "maxValue": data.mask.maxValue
+ } : null,
+ "pixels": {
+ "numBlocksX": data.pixels.numBlocksX,
+ "numBlocksY": data.pixels.numBlocksY,
+ "numBytes": data.pixels.numBytes,
+ "maxValue": data.pixels.maxValue,
+ "noDataValue": data.noDataValue
+ }
+ };
+ };
+
+ var computeUsedBitDepths = function (data) {
+ var numBlocks = data.pixels.numBlocksX * data.pixels.numBlocksY;
+ var bitDepths = {};
+ for (var i = 0; i < numBlocks; i++) {
+ var block = data.pixels.blocks[i];
+ if (block.encoding === 0) {
+ bitDepths.float32 = true;
+ } else if (block.encoding === 1) {
+ bitDepths[block.bitsPerPixel] = true;
+ } else {
+ bitDepths[0] = true;
+ }
+ }
+
+ return Object.keys(bitDepths);
+ };
+
+ var parse = function (input, fp, skipMask) {
+ var data = {};
+
+ // File header
+ var fileIdView = new Uint8Array(input, fp, 10);
+ data.fileIdentifierString = String.fromCharCode.apply(null, fileIdView);
+ if (data.fileIdentifierString.trim() !== "CntZImage") {
+ throw "Unexpected file identifier string: " + data.fileIdentifierString;
+ }
+ fp += 10;
+ var view = new DataView(input, fp, 24);
+ data.fileVersion = view.getInt32(0, true);
+ data.imageType = view.getInt32(4, true);
+ data.height = view.getUint32(8, true);
+ data.width = view.getUint32(12, true);
+ data.maxZError = view.getFloat64(16, true);
+ fp += 24;
+
+ // Mask Header
+ if (!skipMask) {
+ view = new DataView(input, fp, 16);
+ data.mask = {};
+ data.mask.numBlocksY = view.getUint32(0, true);
+ data.mask.numBlocksX = view.getUint32(4, true);
+ data.mask.numBytes = view.getUint32(8, true);
+ data.mask.maxValue = view.getFloat32(12, true);
+ fp += 16;
+
+ // Mask Data
+ if (data.mask.numBytes > 0) {
+ var bitset = new Uint8Array(Math.ceil(data.width * data.height / 8));
+ view = new DataView(input, fp, data.mask.numBytes);
+ var cnt = view.getInt16(0, true);
+ var ip = 2, op = 0;
+ do {
+ if (cnt > 0) {
+ while (cnt--) { bitset[op++] = view.getUint8(ip++); }
+ } else {
+ var val = view.getUint8(ip++);
+ cnt = -cnt;
+ while (cnt--) { bitset[op++] = val; }
+ }
+ cnt = view.getInt16(ip, true);
+ ip += 2;
+ } while (ip < data.mask.numBytes);
+ if (cnt !== -32768 || op < bitset.length) {
+ throw "Unexpected end of mask RLE encoding";
+ }
+ data.mask.bitset = bitset;
+ fp += data.mask.numBytes;
+ }
+ else if ((data.mask.numBytes | data.mask.numBlocksY | data.mask.maxValue) === 0) { // Special case, all nodata
+ data.mask.bitset = new Uint8Array(Math.ceil(data.width * data.height / 8));
+ }
+ }
+
+ // Pixel Header
+ view = new DataView(input, fp, 16);
+ data.pixels = {};
+ data.pixels.numBlocksY = view.getUint32(0, true);
+ data.pixels.numBlocksX = view.getUint32(4, true);
+ data.pixels.numBytes = view.getUint32(8, true);
+ data.pixels.maxValue = view.getFloat32(12, true);
+ fp += 16;
+
+ var numBlocksX = data.pixels.numBlocksX;
+ var numBlocksY = data.pixels.numBlocksY;
+ // the number of blocks specified in the header does not take into account the blocks at the end of
+ // each row/column with a special width/height that make the image complete in case the width is not
+ // evenly divisible by the number of blocks.
+ var actualNumBlocksX = numBlocksX + (data.width % numBlocksX > 0 ? 1 : 0);
+ var actualNumBlocksY = numBlocksY + (data.height % numBlocksY > 0 ? 1 : 0);
+ data.pixels.blocks = new Array(actualNumBlocksX * actualNumBlocksY);
+ var blockI = 0;
+ for (var blockY = 0; blockY < actualNumBlocksY; blockY++) {
+ for (var blockX = 0; blockX < actualNumBlocksX; blockX++) {
+
+ // Block
+ var size = 0;
+ var bytesLeft = input.byteLength - fp;
+ view = new DataView(input, fp, Math.min(10, bytesLeft));
+ var block = {};
+ data.pixels.blocks[blockI++] = block;
+ var headerByte = view.getUint8(0); size++;
+ block.encoding = headerByte & 63;
+ if (block.encoding > 3) {
+ throw "Invalid block encoding (" + block.encoding + ")";
+ }
+ if (block.encoding === 2) {
+ fp++;
+ continue;
+ }
+ if (headerByte !== 0 && headerByte !== 2) {
+ headerByte >>= 6;
+ block.offsetType = headerByte;
+ if (headerByte === 2) {
+ block.offset = view.getInt8(1); size++;
+ } else if (headerByte === 1) {
+ block.offset = view.getInt16(1, true); size += 2;
+ } else if (headerByte === 0) {
+ block.offset = view.getFloat32(1, true); size += 4;
+ } else {
+ throw "Invalid block offset type";
+ }
+
+ if (block.encoding === 1) {
+ headerByte = view.getUint8(size); size++;
+ block.bitsPerPixel = headerByte & 63;
+ headerByte >>= 6;
+ block.numValidPixelsType = headerByte;
+ if (headerByte === 2) {
+ block.numValidPixels = view.getUint8(size); size++;
+ } else if (headerByte === 1) {
+ block.numValidPixels = view.getUint16(size, true); size += 2;
+ } else if (headerByte === 0) {
+ block.numValidPixels = view.getUint32(size, true); size += 4;
+ } else {
+ throw "Invalid valid pixel count type";
+ }
+ }
+ }
+ fp += size;
+
+ if (block.encoding === 3) {
+ continue;
+ }
+
+ var arrayBuf, store8;
+ if (block.encoding === 0) {
+ var numPixels = (data.pixels.numBytes - 1) / 4;
+ if (numPixels !== Math.floor(numPixels)) {
+ throw "uncompressed block has invalid length";
+ }
+ arrayBuf = new ArrayBuffer(numPixels * 4);
+ store8 = new Uint8Array(arrayBuf);
+ store8.set(new Uint8Array(input, fp, numPixels * 4));
+ var rawData = new Float32Array(arrayBuf);
+ block.rawData = rawData;
+ fp += numPixels * 4;
+ } else if (block.encoding === 1) {
+ var dataBytes = Math.ceil(block.numValidPixels * block.bitsPerPixel / 8);
+ var dataWords = Math.ceil(dataBytes / 4);
+ arrayBuf = new ArrayBuffer(dataWords * 4);
+ store8 = new Uint8Array(arrayBuf);
+ store8.set(new Uint8Array(input, fp, dataBytes));
+ block.stuffedData = new Uint32Array(arrayBuf);
+ fp += dataBytes;
+ }
+ }
+ }
+ data.eofOffset = fp;
+ return data;
+ };
+
+ var unstuff = function (src, bitsPerPixel, numPixels, offset, scale, dest, maxValue) {
+ var bitMask = (1 << bitsPerPixel) - 1;
+ var i = 0, o;
+ var bitsLeft = 0;
+ var n, buffer;
+ var nmax = Math.ceil((maxValue - offset) / scale);
+ // get rid of trailing bytes that are already part of next block
+ var numInvalidTailBytes = src.length * 4 - Math.ceil(bitsPerPixel * numPixels / 8);
+ src[src.length - 1] <<= 8 * numInvalidTailBytes;
+
+ for (o = 0; o < numPixels; o++) {
+ if (bitsLeft === 0) {
+ buffer = src[i++];
+ bitsLeft = 32;
+ }
+ if (bitsLeft >= bitsPerPixel) {
+ n = buffer >>> bitsLeft - bitsPerPixel & bitMask;
+ bitsLeft -= bitsPerPixel;
+ } else {
+ var missingBits = bitsPerPixel - bitsLeft;
+ n = (buffer & bitMask) << missingBits & bitMask;
+ buffer = src[i++];
+ bitsLeft = 32 - missingBits;
+ n += buffer >>> bitsLeft;
+ }
+ //pixel values may exceed max due to quantization
+ dest[o] = n < nmax ? offset + n * scale : maxValue;
+ }
+ return dest;
+ };
+
+ return CntZImage;
+})();
+
+//version 2. Supports 2.1, 2.2, 2.3
+var Lerc2Decode = (function () {
+ "use strict";
+ // Note: currently, this module only has an implementation for decoding LERC data, not encoding. The name of
+ // the class was chosen to be future proof, following LercDecode.
+
+ /*****************************************
+ * private static class bitsutffer used by Lerc2Decode
+ *******************************************/
+ var BitStuffer = {
+ //methods ending with 2 are for the new byte order used by Lerc2.3 and above.
+ //originalUnstuff is used to unpack Huffman code table. code is duplicated to unstuffx for performance reasons.
+ unstuff: function (src, dest, bitsPerPixel, numPixels, lutArr, offset, scale, maxValue) {
+ var bitMask = (1 << bitsPerPixel) - 1;
+ var i = 0, o;
+ var bitsLeft = 0;
+ var n, buffer, missingBits, nmax;
+
+ // get rid of trailing bytes that are already part of next block
+ var numInvalidTailBytes = src.length * 4 - Math.ceil(bitsPerPixel * numPixels / 8);
+ src[src.length - 1] <<= 8 * numInvalidTailBytes;
+ if (lutArr) {
+ for (o = 0; o < numPixels; o++) {
+ if (bitsLeft === 0) {
+ buffer = src[i++];
+ bitsLeft = 32;
+ }
+ if (bitsLeft >= bitsPerPixel) {
+ n = buffer >>> bitsLeft - bitsPerPixel & bitMask;
+ bitsLeft -= bitsPerPixel;
+ }
+ else {
+ missingBits = bitsPerPixel - bitsLeft;
+ n = (buffer & bitMask) << missingBits & bitMask;
+ buffer = src[i++];
+ bitsLeft = 32 - missingBits;
+ n += buffer >>> bitsLeft;
+ }
+ dest[o] = lutArr[n];//offset + lutArr[n] * scale;
+ }
+ }
+ else {
+ nmax = Math.ceil((maxValue - offset) / scale);
+ for (o = 0; o < numPixels; o++) {
+ if (bitsLeft === 0) {
+ buffer = src[i++];
+ bitsLeft = 32;
+ }
+ if (bitsLeft >= bitsPerPixel) {
+ n = buffer >>> bitsLeft - bitsPerPixel & bitMask;
+ bitsLeft -= bitsPerPixel;
+ }
+ else {
+ missingBits = bitsPerPixel - bitsLeft;
+ n = (buffer & bitMask) << missingBits & bitMask;
+ buffer = src[i++];
+ bitsLeft = 32 - missingBits;
+ n += buffer >>> bitsLeft;
+ }
+ //pixel values may exceed max due to quantization
+ dest[o] = n < nmax ? offset + n * scale : maxValue;
+ }
+ }
+ },
+
+ unstuffLUT: function (src, bitsPerPixel, numPixels, offset, scale, maxValue) {
+ var bitMask = (1 << bitsPerPixel) - 1;
+ var i = 0, o = 0, missingBits = 0, bitsLeft = 0, n = 0;
+ var buffer;
+ var dest = [];
+
+ // get rid of trailing bytes that are already part of next block
+ var numInvalidTailBytes = src.length * 4 - Math.ceil(bitsPerPixel * numPixels / 8);
+ src[src.length - 1] <<= 8 * numInvalidTailBytes;
+
+ var nmax = Math.ceil((maxValue - offset) / scale);
+ for (o = 0; o < numPixels; o++) {
+ if (bitsLeft === 0) {
+ buffer = src[i++];
+ bitsLeft = 32;
+ }
+ if (bitsLeft >= bitsPerPixel) {
+ n = buffer >>> bitsLeft - bitsPerPixel & bitMask;
+ bitsLeft -= bitsPerPixel;
+ } else {
+ missingBits = bitsPerPixel - bitsLeft;
+ n = (buffer & bitMask) << missingBits & bitMask;
+ buffer = src[i++];
+ bitsLeft = 32 - missingBits;
+ n += buffer >>> bitsLeft;
+ }
+ //dest.push(n);
+ dest[o] = n < nmax ? offset + n * scale : maxValue;
+ }
+ dest.unshift(offset);//1st one
+ return dest;
+ },
+
+ unstuff2: function (src, dest, bitsPerPixel, numPixels, lutArr, offset, scale, maxValue) {
+ var bitMask = (1 << bitsPerPixel) - 1;
+ var i = 0, o;
+ var bitsLeft = 0, bitPos = 0;
+ var n, buffer, missingBits;
+ if (lutArr) {
+ for (o = 0; o < numPixels; o++) {
+ if (bitsLeft === 0) {
+ buffer = src[i++];
+ bitsLeft = 32;
+ bitPos = 0;
+ }
+ if (bitsLeft >= bitsPerPixel) {
+ n = buffer >>> bitPos & bitMask;
+ bitsLeft -= bitsPerPixel;
+ bitPos += bitsPerPixel;
+ } else {
+ missingBits = bitsPerPixel - bitsLeft;
+ n = buffer >>> bitPos & bitMask;
+ buffer = src[i++];
+ bitsLeft = 32 - missingBits;
+ n |= (buffer & (1 << missingBits) - 1) << bitsPerPixel - missingBits;
+ bitPos = missingBits;
+ }
+ dest[o] = lutArr[n];
+ }
+ }
+ else {
+ var nmax = Math.ceil((maxValue - offset) / scale);
+ for (o = 0; o < numPixels; o++) {
+ if (bitsLeft === 0) {
+ buffer = src[i++];
+ bitsLeft = 32;
+ bitPos = 0;
+ }
+ if (bitsLeft >= bitsPerPixel) {
+ //no unsigned left shift
+ n = buffer >>> bitPos & bitMask;
+ bitsLeft -= bitsPerPixel;
+ bitPos += bitsPerPixel;
+ } else {
+ missingBits = bitsPerPixel - bitsLeft;
+ n = buffer >>> bitPos & bitMask;//((buffer & bitMask) << missingBits) & bitMask;
+ buffer = src[i++];
+ bitsLeft = 32 - missingBits;
+ n |= (buffer & (1 << missingBits) - 1) << bitsPerPixel - missingBits;
+ bitPos = missingBits;
+ }
+ //pixel values may exceed max due to quantization
+ dest[o] = n < nmax ? offset + n * scale : maxValue;
+ }
+ }
+ return dest;
+ },
+
+ unstuffLUT2: function (src, bitsPerPixel, numPixels, offset, scale, maxValue) {
+ var bitMask = (1 << bitsPerPixel) - 1;
+ var i = 0, o = 0, missingBits = 0, bitsLeft = 0, n = 0, bitPos = 0;
+ var buffer;
+ var dest = [];
+ var nmax = Math.ceil((maxValue - offset) / scale);
+ for (o = 0; o < numPixels; o++) {
+ if (bitsLeft === 0) {
+ buffer = src[i++];
+ bitsLeft = 32;
+ bitPos = 0;
+ }
+ if (bitsLeft >= bitsPerPixel) {
+ //no unsigned left shift
+ n = buffer >>> bitPos & bitMask;
+ bitsLeft -= bitsPerPixel;
+ bitPos += bitsPerPixel;
+ } else {
+ missingBits = bitsPerPixel - bitsLeft;
+ n = buffer >>> bitPos & bitMask;//((buffer & bitMask) << missingBits) & bitMask;
+ buffer = src[i++];
+ bitsLeft = 32 - missingBits;
+ n |= (buffer & (1 << missingBits) - 1) << bitsPerPixel - missingBits;
+ bitPos = missingBits;
+ }
+ //dest.push(n);
+ dest[o] = n < nmax ? offset + n * scale : maxValue;
+ }
+ dest.unshift(offset);
+ return dest;
+ },
+
+ originalUnstuff: function (src, dest, bitsPerPixel, numPixels) {
+ var bitMask = (1 << bitsPerPixel) - 1;
+ var i = 0, o;
+ var bitsLeft = 0;
+ var n, buffer, missingBits;
+
+ // get rid of trailing bytes that are already part of next block
+ var numInvalidTailBytes = src.length * 4 - Math.ceil(bitsPerPixel * numPixels / 8);
+ src[src.length - 1] <<= 8 * numInvalidTailBytes;
+
+ for (o = 0; o < numPixels; o++) {
+ if (bitsLeft === 0) {
+ buffer = src[i++];
+ bitsLeft = 32;
+ }
+ if (bitsLeft >= bitsPerPixel) {
+ n = buffer >>> bitsLeft - bitsPerPixel & bitMask;
+ bitsLeft -= bitsPerPixel;
+ }
+ else {
+ missingBits = bitsPerPixel - bitsLeft;
+ n = (buffer & bitMask) << missingBits & bitMask;
+ buffer = src[i++];
+ bitsLeft = 32 - missingBits;
+ n += buffer >>> bitsLeft;
+ }
+ dest[o] = n;
+ }
+ return dest;
+ },
+
+ originalUnstuff2: function (src, dest, bitsPerPixel, numPixels) {
+ var bitMask = (1 << bitsPerPixel) - 1;
+ var i = 0, o;
+ var bitsLeft = 0, bitPos = 0;
+ var n, buffer, missingBits;
+ //micro-optimizations
+ for (o = 0; o < numPixels; o++) {
+ if (bitsLeft === 0) {
+ buffer = src[i++];
+ bitsLeft = 32;
+ bitPos = 0;
+ }
+ if (bitsLeft >= bitsPerPixel) {
+ //no unsigned left shift
+ n = buffer >>> bitPos & bitMask;
+ bitsLeft -= bitsPerPixel;
+ bitPos += bitsPerPixel;
+ } else {
+ missingBits = bitsPerPixel - bitsLeft;
+ n = buffer >>> bitPos & bitMask;//((buffer & bitMask) << missingBits) & bitMask;
+ buffer = src[i++];
+ bitsLeft = 32 - missingBits;
+ n |= (buffer & (1 << missingBits) - 1) << bitsPerPixel - missingBits;
+ bitPos = missingBits;
+ }
+ dest[o] = n;
+ }
+ return dest;
+ }
+ };
+
+ /*****************************************
+ *private static class used by Lerc2Decode
+ ******************************************/
+ var Lerc2Helpers = {
+ HUFFMAN_LUT_BITS_MAX: 12, //use 2^12 lut, treat it like constant
+ computeChecksumFletcher32: function (input) {
+
+ var sum1 = 0xffff, sum2 = 0xffff;
+ var len = input.length;
+ var words = Math.floor(len / 2);
+ var i = 0;
+ while (words) {
+ var tlen = words >= 359 ? 359 : words;
+ words -= tlen;
+ do {
+ sum1 += input[i++] << 8;
+ sum2 += sum1 += input[i++];
+ } while (--tlen);
+
+ sum1 = (sum1 & 0xffff) + (sum1 >>> 16);
+ sum2 = (sum2 & 0xffff) + (sum2 >>> 16);
+ }
+
+ // add the straggler byte if it exists
+ if (len & 1) {
+ sum2 += sum1 += input[i] << 8;
+ }
+ // second reduction step to reduce sums to 16 bits
+ sum1 = (sum1 & 0xffff) + (sum1 >>> 16);
+ sum2 = (sum2 & 0xffff) + (sum2 >>> 16);
+
+ return (sum2 << 16 | sum1) >>> 0;
+ },
+
+ readHeaderInfo: function (input, data) {
+ var ptr = data.ptr;
+ var fileIdView = new Uint8Array(input, ptr, 6);
+ var headerInfo = {};
+ headerInfo.fileIdentifierString = String.fromCharCode.apply(null, fileIdView);
+ if (headerInfo.fileIdentifierString.lastIndexOf("Lerc2", 0) !== 0) {
+ throw "Unexpected file identifier string (expect Lerc2 ): " + headerInfo.fileIdentifierString;
+ }
+ ptr += 6;
+ var view = new DataView(input, ptr, 8);
+ var fileVersion = view.getInt32(0, true);
+ headerInfo.fileVersion = fileVersion;
+ ptr += 4;
+ if (fileVersion >= 3) {
+ headerInfo.checksum = view.getUint32(4, true); //nrows
+ ptr += 4;
+ }
+
+ //keys start from here
+ view = new DataView(input, ptr, 12);
+ headerInfo.height = view.getUint32(0, true); //nrows
+ headerInfo.width = view.getUint32(4, true); //ncols
+ ptr += 8;
+ if (fileVersion >= 4) {
+ headerInfo.numDims = view.getUint32(8, true);
+ ptr += 4;
+ }
+ else {
+ headerInfo.numDims = 1;
+ }
+
+ view = new DataView(input, ptr, 40);
+ headerInfo.numValidPixel = view.getUint32(0, true);
+ headerInfo.microBlockSize = view.getInt32(4, true);
+ headerInfo.blobSize = view.getInt32(8, true);
+ headerInfo.imageType = view.getInt32(12, true);
+
+ headerInfo.maxZError = view.getFloat64(16, true);
+ headerInfo.zMin = view.getFloat64(24, true);
+ headerInfo.zMax = view.getFloat64(32, true);
+ ptr += 40;
+ data.headerInfo = headerInfo;
+ data.ptr = ptr;
+
+ var checksum, keyLength;
+ if (fileVersion >= 3) {
+ keyLength = fileVersion >= 4 ? 52 : 48;
+ checksum = this.computeChecksumFletcher32(new Uint8Array(input, ptr - keyLength, headerInfo.blobSize - 14));
+ if (checksum !== headerInfo.checksum) {
+ throw "Checksum failed.";
+ }
+ }
+ return true;
+ },
+
+ checkMinMaxRanges: function (input, data) {
+ var headerInfo = data.headerInfo;
+ var OutPixelTypeArray = this.getDataTypeArray(headerInfo.imageType);
+ var rangeBytes = headerInfo.numDims * this.getDataTypeSize(headerInfo.imageType);
+ var minValues = this.readSubArray(input, data.ptr, OutPixelTypeArray, rangeBytes);
+ var maxValues = this.readSubArray(input, data.ptr + rangeBytes, OutPixelTypeArray, rangeBytes);
+ data.ptr += 2 * rangeBytes;
+ var i, equal = true;
+ for (i = 0; i < headerInfo.numDims; i++) {
+ if (minValues[i] !== maxValues[i]) {
+ equal = false;
+ break;
+ }
+ }
+ headerInfo.minValues = minValues;
+ headerInfo.maxValues = maxValues;
+ return equal;
+ },
+
+ readSubArray: function (input, ptr, OutPixelTypeArray, numBytes) {
+ var rawData;
+ if (OutPixelTypeArray === Uint8Array) {
+ rawData = new Uint8Array(input, ptr, numBytes);
+ }
+ else {
+ var arrayBuf = new ArrayBuffer(numBytes);
+ var store8 = new Uint8Array(arrayBuf);
+ store8.set(new Uint8Array(input, ptr, numBytes));
+ rawData = new OutPixelTypeArray(arrayBuf);
+ }
+ return rawData;
+ },
+
+ readMask: function (input, data) {
+ var ptr = data.ptr;
+ var headerInfo = data.headerInfo;
+ var numPixels = headerInfo.width * headerInfo.height;
+ var numValidPixel = headerInfo.numValidPixel;
+
+ var view = new DataView(input, ptr, 4);
+ var mask = {};
+ mask.numBytes = view.getUint32(0, true);
+ ptr += 4;
+
+ // Mask Data
+ if ((0 === numValidPixel || numPixels === numValidPixel) && 0 !== mask.numBytes) {
+ throw "invalid mask";
+ }
+ var bitset, resultMask;
+ if (numValidPixel === 0) {
+ bitset = new Uint8Array(Math.ceil(numPixels / 8));
+ mask.bitset = bitset;
+ resultMask = new Uint8Array(numPixels);
+ data.pixels.resultMask = resultMask;
+ ptr += mask.numBytes;
+ }// ????? else if (data.mask.numBytes > 0 && data.mask.numBytes< data.numValidPixel) {
+ else if (mask.numBytes > 0) {
+ bitset = new Uint8Array(Math.ceil(numPixels / 8));
+ view = new DataView(input, ptr, mask.numBytes);
+ var cnt = view.getInt16(0, true);
+ var ip = 2, op = 0, val = 0;
+ do {
+ if (cnt > 0) {
+ while (cnt--) { bitset[op++] = view.getUint8(ip++); }
+ } else {
+ val = view.getUint8(ip++);
+ cnt = -cnt;
+ while (cnt--) { bitset[op++] = val; }
+ }
+ cnt = view.getInt16(ip, true);
+ ip += 2;
+ } while (ip < mask.numBytes);
+ if (cnt !== -32768 || op < bitset.length) {
+ throw "Unexpected end of mask RLE encoding";
+ }
+
+ resultMask = new Uint8Array(numPixels);
+ var mb = 0, k = 0;
+
+ for (k = 0; k < numPixels; k++) {
+ if (k & 7) {
+ mb = bitset[k >> 3];
+ mb <<= k & 7;
+ }
+ else {
+ mb = bitset[k >> 3];
+ }
+ if (mb & 128) {
+ resultMask[k] = 1;
+ }
+ }
+ data.pixels.resultMask = resultMask;
+
+ mask.bitset = bitset;
+ ptr += mask.numBytes;
+ }
+ data.ptr = ptr;
+ data.mask = mask;
+ return true;
+ },
+
+ readDataOneSweep: function (input, data, OutPixelTypeArray) {
+ var ptr = data.ptr;
+ var headerInfo = data.headerInfo;
+ var numDims = headerInfo.numDims;
+ var numPixels = headerInfo.width * headerInfo.height;
+ var imageType = headerInfo.imageType;
+ var numBytes = headerInfo.numValidPixel * Lerc2Helpers.getDataTypeSize(imageType) * numDims;
+ //data.pixels.numBytes = numBytes;
+ var rawData;
+ var mask = data.pixels.resultMask;
+ if (OutPixelTypeArray === Uint8Array) {
+ rawData = new Uint8Array(input, ptr, numBytes);
+ }
+ else {
+ var arrayBuf = new ArrayBuffer(numBytes);
+ var store8 = new Uint8Array(arrayBuf);
+ store8.set(new Uint8Array(input, ptr, numBytes));
+ rawData = new OutPixelTypeArray(arrayBuf);
+ }
+ if (rawData.length === numPixels * numDims) {
+ data.pixels.resultPixels = rawData;
+ }
+ else //mask
+ {
+ data.pixels.resultPixels = new OutPixelTypeArray(numPixels * numDims);
+ var z = 0, k = 0, i = 0, nStart = 0;
+ if (numDims > 1) {
+ for (i = 0; i < numDims; i++) {
+ nStart = i * numPixels;
+ for (k = 0; k < numPixels; k++) {
+ if (mask[k]) {
+ data.pixels.resultPixels[nStart + k] = rawData[z++];
+ }
+ }
+ }
+ }
+ else {
+ for (k = 0; k < numPixels; k++) {
+ if (mask[k]) {
+ data.pixels.resultPixels[k] = rawData[z++];
+ }
+ }
+ }
+ }
+ ptr += numBytes;
+ data.ptr = ptr; //return data;
+ return true;
+ },
+
+ readHuffmanTree: function (input, data) {
+ var BITS_MAX = this.HUFFMAN_LUT_BITS_MAX; //8 is slow for the large test image
+ //var size_max = 1 << BITS_MAX;
+ /* ************************
+ * reading code table
+ *************************/
+ var view = new DataView(input, data.ptr, 16);
+ data.ptr += 16;
+ var version = view.getInt32(0, true);
+ if (version < 2) {
+ throw "unsupported Huffman version";
+ }
+ var size = view.getInt32(4, true);
+ var i0 = view.getInt32(8, true);
+ var i1 = view.getInt32(12, true);
+ if (i0 >= i1) {
+ return false;
+ }
+ var blockDataBuffer = new Uint32Array(i1 - i0);
+ Lerc2Helpers.decodeBits(input, data, blockDataBuffer);
+ var codeTable = []; //size
+ var i, j, k, len;
+
+ for (i = i0; i < i1; i++) {
+ j = i - (i < size ? 0 : size);//wrap around
+ codeTable[j] = { first: blockDataBuffer[i - i0], second: null };
+ }
+
+ var dataBytes = input.byteLength - data.ptr;
+ var dataWords = Math.ceil(dataBytes / 4);
+ var arrayBuf = new ArrayBuffer(dataWords * 4);
+ var store8 = new Uint8Array(arrayBuf);
+ store8.set(new Uint8Array(input, data.ptr, dataBytes));
+ var stuffedData = new Uint32Array(arrayBuf); //must start from x*4
+ var bitPos = 0, word, srcPtr = 0;
+ word = stuffedData[0];
+ for (i = i0; i < i1; i++) {
+ j = i - (i < size ? 0 : size);//wrap around
+ len = codeTable[j].first;
+ if (len > 0) {
+ codeTable[j].second = word << bitPos >>> 32 - len;
+
+ if (32 - bitPos >= len) {
+ bitPos += len;
+ if (bitPos === 32) {
+ bitPos = 0;
+ srcPtr++;
+ word = stuffedData[srcPtr];
+ }
+ }
+ else {
+ bitPos += len - 32;
+ srcPtr++;
+ word = stuffedData[srcPtr];
+ codeTable[j].second |= word >>> 32 - bitPos;
+ }
+ }
+ }
+
+ //finished reading code table
+
+ /* ************************
+ * building lut
+ *************************/
+ var numBitsLUT = 0, numBitsLUTQick = 0;
+ var tree = new TreeNode();
+ for (i = 0; i < codeTable.length; i++) {
+ if (codeTable[i] !== undefined) {
+ numBitsLUT = Math.max(numBitsLUT, codeTable[i].first);
+ }
+ }
+ if (numBitsLUT >= BITS_MAX) {
+ numBitsLUTQick = BITS_MAX;
+ }
+ else {
+ numBitsLUTQick = numBitsLUT;
+ }
+ // for debugging purpose
+ // if (numBitsLUT >= 30) {
+ // console.log("WARning, large NUM LUT BITS IS " + numBitsLUT);
+ // }
+ var decodeLut = [], entry, code, numEntries, jj, currentBit, node;
+ for (i = i0; i < i1; i++) {
+ j = i - (i < size ? 0 : size);//wrap around
+ len = codeTable[j].first;
+ if (len > 0) {
+ entry = [len, j];
+ if (len <= numBitsLUTQick) {
+ code = codeTable[j].second << numBitsLUTQick - len;
+ numEntries = 1 << numBitsLUTQick - len;
+ for (k = 0; k < numEntries; k++) {
+ decodeLut[code | k] = entry;
+ }
+ }
+ else {
+ //build tree
+ code = codeTable[j].second;
+ node = tree;
+ for (jj = len - 1; jj >= 0; jj--) {
+ currentBit = code >>> jj & 1; //no left shift as length could be 30,31
+ if (currentBit) {
+ if (!node.right) {
+ node.right = new TreeNode();
+ }
+ node = node.right;
+ }
+ else {
+ if (!node.left) {
+ node.left = new TreeNode();
+ }
+ node = node.left;
+ }
+ if (jj === 0 && !node.val) {
+ node.val = entry[1];
+ }
+ }
+ }
+ }
+ }
+ return {
+ decodeLut: decodeLut,
+ numBitsLUTQick: numBitsLUTQick,
+ numBitsLUT: numBitsLUT,
+ tree: tree,
+ stuffedData: stuffedData,
+ srcPtr: srcPtr,
+ bitPos: bitPos
+ };
+ },
+
+ readHuffman: function (input, data, OutPixelTypeArray) {
+ var headerInfo = data.headerInfo;
+ var numDims = headerInfo.numDims;
+ var height = data.headerInfo.height;
+ var width = data.headerInfo.width;
+ var numPixels = width * height;
+ //var size_max = 1 << BITS_MAX;
+ /* ************************
+ * reading huffman structure info
+ *************************/
+ var huffmanInfo = this.readHuffmanTree(input, data);
+ var decodeLut = huffmanInfo.decodeLut;
+ var tree = huffmanInfo.tree;
+ //stuffedData includes huffman headers
+ var stuffedData = huffmanInfo.stuffedData;
+ var srcPtr = huffmanInfo.srcPtr;
+ var bitPos = huffmanInfo.bitPos;
+ var numBitsLUTQick = huffmanInfo.numBitsLUTQick;
+ var numBitsLUT = huffmanInfo.numBitsLUT;
+ var offset = data.headerInfo.imageType === 0 ? 128 : 0;
+ /*************************
+ * decode
+ ***************************/
+ var node, val, delta, mask = data.pixels.resultMask, valTmp, valTmpQuick, currentBit;
+ var i, j, k, ii;
+ var prevVal = 0;
+ if (bitPos > 0) {
+ srcPtr++;
+ bitPos = 0;
+ }
+ var word = stuffedData[srcPtr];
+ var deltaEncode = data.encodeMode === 1;
+ var resultPixelsAllDim = new OutPixelTypeArray(numPixels * numDims);
+ var resultPixels = resultPixelsAllDim;
+ var iDim;
+ for (iDim = 0; iDim < headerInfo.numDims; iDim++) {
+ if (numDims > 1) {
+ //get the mem block of current dimension
+ resultPixels = new OutPixelTypeArray(resultPixelsAllDim.buffer, numPixels * iDim, numPixels);
+ prevVal = 0;
+ }
+ if (data.headerInfo.numValidPixel === width * height) { //all valid
+ for (k = 0, i = 0; i < height; i++) {
+ for (j = 0; j < width; j++, k++) {
+ val = 0;
+ valTmp = word << bitPos >>> 32 - numBitsLUTQick;
+ valTmpQuick = valTmp;// >>> deltaBits;
+ if (32 - bitPos < numBitsLUTQick) {
+ valTmp |= stuffedData[srcPtr + 1] >>> 64 - bitPos - numBitsLUTQick;
+ valTmpQuick = valTmp;// >>> deltaBits;
+ }
+ if (decodeLut[valTmpQuick]) // if there, move the correct number of bits and done
+ {
+ val = decodeLut[valTmpQuick][1];
+ bitPos += decodeLut[valTmpQuick][0];
+ }
+ else {
+ valTmp = word << bitPos >>> 32 - numBitsLUT;
+ valTmpQuick = valTmp;// >>> deltaBits;
+ if (32 - bitPos < numBitsLUT) {
+ valTmp |= stuffedData[srcPtr + 1] >>> 64 - bitPos - numBitsLUT;
+ valTmpQuick = valTmp;// >>> deltaBits;
+ }
+ node = tree;
+ for (ii = 0; ii < numBitsLUT; ii++) {
+ currentBit = valTmp >>> numBitsLUT - ii - 1 & 1;
+ node = currentBit ? node.right : node.left;
+ if (!(node.left || node.right)) {
+ val = node.val;
+ bitPos = bitPos + ii + 1;
+ break;
+ }
+ }
+ }
+
+ if (bitPos >= 32) {
+ bitPos -= 32;
+ srcPtr++;
+ word = stuffedData[srcPtr];
+ }
+
+ delta = val - offset;
+ if (deltaEncode) {
+ if (j > 0) {
+ delta += prevVal; // use overflow
+ }
+ else if (i > 0) {
+ delta += resultPixels[k - width];
+ }
+ else {
+ delta += prevVal;
+ }
+ delta &= 0xFF; //overflow
+ resultPixels[k] = delta;//overflow
+ prevVal = delta;
+ }
+ else {
+ resultPixels[k] = delta;
+ }
+ }
+ }
+ }
+ else { //not all valid, use mask
+ for (k = 0, i = 0; i < height; i++) {
+ for (j = 0; j < width; j++, k++) {
+ if (mask[k]) {
+ val = 0;
+ valTmp = word << bitPos >>> 32 - numBitsLUTQick;
+ valTmpQuick = valTmp;// >>> deltaBits;
+ if (32 - bitPos < numBitsLUTQick) {
+ valTmp |= stuffedData[srcPtr + 1] >>> 64 - bitPos - numBitsLUTQick;
+ valTmpQuick = valTmp;// >>> deltaBits;
+ }
+ if (decodeLut[valTmpQuick]) // if there, move the correct number of bits and done
+ {
+ val = decodeLut[valTmpQuick][1];
+ bitPos += decodeLut[valTmpQuick][0];
+ }
+ else {
+ valTmp = word << bitPos >>> 32 - numBitsLUT;
+ valTmpQuick = valTmp;// >>> deltaBits;
+ if (32 - bitPos < numBitsLUT) {
+ valTmp |= stuffedData[srcPtr + 1] >>> 64 - bitPos - numBitsLUT;
+ valTmpQuick = valTmp;// >>> deltaBits;
+ }
+ node = tree;
+ for (ii = 0; ii < numBitsLUT; ii++) {
+ currentBit = valTmp >>> numBitsLUT - ii - 1 & 1;
+ node = currentBit ? node.right : node.left;
+ if (!(node.left || node.right)) {
+ val = node.val;
+ bitPos = bitPos + ii + 1;
+ break;
+ }
+ }
+ }
+
+ if (bitPos >= 32) {
+ bitPos -= 32;
+ srcPtr++;
+ word = stuffedData[srcPtr];
+ }
+
+ delta = val - offset;
+ if (deltaEncode) {
+ if (j > 0 && mask[k - 1]) {
+ delta += prevVal; // use overflow
+ }
+ else if (i > 0 && mask[k - width]) {
+ delta += resultPixels[k - width];
+ }
+ else {
+ delta += prevVal;
+ }
+
+ delta &= 0xFF; //overflow
+ resultPixels[k] = delta;//overflow
+ prevVal = delta;
+ }
+ else {
+ resultPixels[k] = delta;
+ }
+ }
+ }
+ }
+ }
+ data.ptr = data.ptr + (srcPtr + 1) * 4 + (bitPos > 0 ? 4 : 0);
+ }
+ data.pixels.resultPixels = resultPixelsAllDim;
+ },
+
+ decodeBits: function (input, data, blockDataBuffer, offset, iDim) {
+ {
+ //bitstuff encoding is 3
+ var headerInfo = data.headerInfo;
+ var fileVersion = headerInfo.fileVersion;
+ //var block = {};
+ var blockPtr = 0;
+ var viewByteLength = input.byteLength - data.ptr >= 5 ? 5 : input.byteLength - data.ptr;
+ var view = new DataView(input, data.ptr, viewByteLength);
+ var headerByte = view.getUint8(0);
+ blockPtr++;
+ var bits67 = headerByte >> 6;
+ var n = bits67 === 0 ? 4 : 3 - bits67;
+ var doLut = (headerByte & 32) > 0 ? true : false;//5th bit
+ var numBits = headerByte & 31;
+ var numElements = 0;
+ if (n === 1) {
+ numElements = view.getUint8(blockPtr); blockPtr++;
+ } else if (n === 2) {
+ numElements = view.getUint16(blockPtr, true); blockPtr += 2;
+ } else if (n === 4) {
+ numElements = view.getUint32(blockPtr, true); blockPtr += 4;
+ } else {
+ throw "Invalid valid pixel count type";
+ }
+ //fix: huffman codes are bit stuffed, but not bound by data's max value, so need to use originalUnstuff
+ //offset = offset || 0;
+ var scale = 2 * headerInfo.maxZError;
+ var stuffedData, arrayBuf, store8, dataBytes, dataWords;
+ var lutArr, lutData, lutBytes, lutBitsPerElement, bitsPerPixel;
+ var zMax = headerInfo.numDims > 1 ? headerInfo.maxValues[iDim] : headerInfo.zMax;
+ if (doLut) {
+ data.counter.lut++;
+ lutBytes = view.getUint8(blockPtr);
+ lutBitsPerElement = numBits;
+ blockPtr++;
+ dataBytes = Math.ceil((lutBytes - 1) * numBits / 8);
+ dataWords = Math.ceil(dataBytes / 4);
+ arrayBuf = new ArrayBuffer(dataWords * 4);
+ store8 = new Uint8Array(arrayBuf);
+
+ data.ptr += blockPtr;
+ store8.set(new Uint8Array(input, data.ptr, dataBytes));
+
+ lutData = new Uint32Array(arrayBuf);
+ data.ptr += dataBytes;
+
+ bitsPerPixel = 0;
+ while (lutBytes - 1 >>> bitsPerPixel) {
+ bitsPerPixel++;
+ }
+ dataBytes = Math.ceil(numElements * bitsPerPixel / 8);
+ dataWords = Math.ceil(dataBytes / 4);
+ arrayBuf = new ArrayBuffer(dataWords * 4);
+ store8 = new Uint8Array(arrayBuf);
+ store8.set(new Uint8Array(input, data.ptr, dataBytes));
+ stuffedData = new Uint32Array(arrayBuf);
+ data.ptr += dataBytes;
+ if (fileVersion >= 3) {
+ lutArr = BitStuffer.unstuffLUT2(lutData, numBits, lutBytes - 1, offset, scale, zMax);
+ }
+ else {
+ lutArr = BitStuffer.unstuffLUT(lutData, numBits, lutBytes - 1, offset, scale, zMax);
+ }
+ //lutArr.unshift(0);
+ if (fileVersion >= 3) {
+ //BitStuffer.unstuff2(block, blockDataBuffer, headerInfo.zMax);
+ BitStuffer.unstuff2(stuffedData, blockDataBuffer, bitsPerPixel, numElements, lutArr);
+ }
+ else {
+ BitStuffer.unstuff(stuffedData, blockDataBuffer, bitsPerPixel, numElements, lutArr);
+ }
+ }
+ else {
+ //console.debug("bitstuffer");
+ data.counter.bitstuffer++;
+ bitsPerPixel = numBits;
+ data.ptr += blockPtr;
+ if (bitsPerPixel > 0) {
+ dataBytes = Math.ceil(numElements * bitsPerPixel / 8);
+ dataWords = Math.ceil(dataBytes / 4);
+ arrayBuf = new ArrayBuffer(dataWords * 4);
+ store8 = new Uint8Array(arrayBuf);
+ store8.set(new Uint8Array(input, data.ptr, dataBytes));
+ stuffedData = new Uint32Array(arrayBuf);
+ data.ptr += dataBytes;
+ if (fileVersion >= 3) {
+ if (offset == null) {
+ BitStuffer.originalUnstuff2(stuffedData, blockDataBuffer, bitsPerPixel, numElements);
+ }
+ else {
+ BitStuffer.unstuff2(stuffedData, blockDataBuffer, bitsPerPixel, numElements, false, offset, scale, zMax);
+ }
+ }
+ else {
+ if (offset == null) {
+ BitStuffer.originalUnstuff(stuffedData, blockDataBuffer, bitsPerPixel, numElements);
+ }
+ else {
+ BitStuffer.unstuff(stuffedData, blockDataBuffer, bitsPerPixel, numElements, false, offset, scale, zMax);
+ }
+ }
+ }
+ }
+ }
+
+ },
+
+ readTiles: function (input, data, OutPixelTypeArray) {
+ var headerInfo = data.headerInfo;
+ var width = headerInfo.width;
+ var height = headerInfo.height;
+ var microBlockSize = headerInfo.microBlockSize;
+ var imageType = headerInfo.imageType;
+ var dataTypeSize = Lerc2Helpers.getDataTypeSize(imageType);
+ var numBlocksX = Math.ceil(width / microBlockSize);
+ var numBlocksY = Math.ceil(height / microBlockSize);
+ data.pixels.numBlocksY = numBlocksY;
+ data.pixels.numBlocksX = numBlocksX;
+ data.pixels.ptr = 0;
+ var row = 0, col = 0, blockY = 0, blockX = 0, thisBlockHeight = 0, thisBlockWidth = 0, bytesLeft = 0, headerByte = 0, bits67 = 0, testCode = 0, outPtr = 0, outStride = 0, numBytes = 0, bytesleft = 0, z = 0, blockPtr = 0;
+ var view, block, arrayBuf, store8, rawData;
+ var blockEncoding;
+ var blockDataBuffer = new OutPixelTypeArray(microBlockSize * microBlockSize);
+ var lastBlockHeight = height % microBlockSize || microBlockSize;
+ var lastBlockWidth = width % microBlockSize || microBlockSize;
+ var offsetType, offset;
+ var numDims = headerInfo.numDims, iDim;
+ var mask = data.pixels.resultMask;
+ var resultPixels = data.pixels.resultPixels;
+ var fileVersion = headerInfo.fileVersion;
+ var fileVersionCheckNum = fileVersion >= 5 ? 14 : 15;
+ var isDiffEncoding;
+ var zMax = headerInfo.zMax;
+ //var resultPixelsAllDim = resultPixels;
+ var resultPixelsPrevDim;
+ for (blockY = 0; blockY < numBlocksY; blockY++) {
+ thisBlockHeight = blockY !== numBlocksY - 1 ? microBlockSize : lastBlockHeight;
+ for (blockX = 0; blockX < numBlocksX; blockX++) {
+ //console.debug("y" + blockY + " x" + blockX);
+ thisBlockWidth = blockX !== numBlocksX - 1 ? microBlockSize : lastBlockWidth;
+
+ outPtr = blockY * width * microBlockSize + blockX * microBlockSize;
+ outStride = width - thisBlockWidth;
+
+ for (iDim = 0; iDim < numDims; iDim++) {
+ if (numDims > 1) {
+ resultPixelsPrevDim = resultPixels;
+ outPtr = blockY * width * microBlockSize + blockX * microBlockSize;
+ resultPixels = new OutPixelTypeArray(data.pixels.resultPixels.buffer, width * height * iDim * dataTypeSize, width * height);
+ zMax = headerInfo.maxValues[iDim];
+ } else {
+ resultPixelsPrevDim = null;
+ }
+ bytesLeft = input.byteLength - data.ptr;
+ view = new DataView(input, data.ptr, Math.min(10, bytesLeft));
+ block = {};
+ blockPtr = 0;
+ headerByte = view.getUint8(0);
+ blockPtr++;
+ isDiffEncoding = headerInfo.fileVersion >= 5 ? headerByte & 4 : 0;
+ bits67 = headerByte >> 6 & 0xFF;
+ testCode = headerByte >> 2 & fileVersionCheckNum; // use bits 2345 for integrity check
+ if (testCode !== (blockX * microBlockSize >> 3 & fileVersionCheckNum)) {
+ throw "integrity issue";
+ }
+
+ if (isDiffEncoding && iDim === 0) {
+ throw "integrity issue";
+ }
+
+ blockEncoding = headerByte & 3;
+ if (blockEncoding > 3) {
+ data.ptr += blockPtr;
+ throw "Invalid block encoding (" + blockEncoding + ")";
+ }
+ else if (blockEncoding === 2) { //constant 0
+ if (isDiffEncoding) {
+ if (mask) {
+ for (row = 0; row < thisBlockHeight; row++) {
+ for (col = 0; col < thisBlockWidth; col++) {
+ if (mask[outPtr]) {
+ resultPixels[outPtr] = resultPixelsPrevDim[outPtr];
+ }
+ outPtr++;
+ }
+ }
+ }
+ else {
+ for (row = 0; row < thisBlockHeight; row++) {
+ for (col = 0; col < thisBlockWidth; col++) {
+ resultPixels[outPtr] = resultPixelsPrevDim[outPtr];
+ outPtr++;
+ }
+ }
+ }
+ }
+ data.counter.constant++;
+ data.ptr += blockPtr;
+ continue;
+ }
+ else if (blockEncoding === 0) { //uncompressed
+ if (isDiffEncoding) {
+ // doesn't make sense, should not happen
+ throw "integrity issue";
+ }
+ data.counter.uncompressed++;
+ data.ptr += blockPtr;
+ numBytes = thisBlockHeight * thisBlockWidth * dataTypeSize;
+ bytesleft = input.byteLength - data.ptr;
+ numBytes = numBytes < bytesleft ? numBytes : bytesleft;
+ //bit alignment
+ arrayBuf = new ArrayBuffer(numBytes % dataTypeSize === 0 ? numBytes : numBytes + dataTypeSize - numBytes % dataTypeSize);
+ store8 = new Uint8Array(arrayBuf);
+ store8.set(new Uint8Array(input, data.ptr, numBytes));
+ rawData = new OutPixelTypeArray(arrayBuf);
+ z = 0;
+ if (mask) {
+ for (row = 0; row < thisBlockHeight; row++) {
+ for (col = 0; col < thisBlockWidth; col++) {
+ if (mask[outPtr]) {
+ resultPixels[outPtr] = rawData[z++];
+ }
+ outPtr++;
+ }
+ outPtr += outStride;
+ }
+ }
+ else {//all valid
+ for (row = 0; row < thisBlockHeight; row++) {
+ for (col = 0; col < thisBlockWidth; col++) {
+ resultPixels[outPtr++] = rawData[z++];
+ }
+ outPtr += outStride;
+ }
+ }
+ data.ptr += z * dataTypeSize;
+ }
+ else { //1 or 3
+ offsetType = Lerc2Helpers.getDataTypeUsed(isDiffEncoding && imageType < 6 ? 4 : imageType, bits67);
+ offset = Lerc2Helpers.getOnePixel(block, blockPtr, offsetType, view);
+ blockPtr += Lerc2Helpers.getDataTypeSize(offsetType);
+ if (blockEncoding === 3) //constant offset value
+ {
+ data.ptr += blockPtr;
+ data.counter.constantoffset++;
+ //you can delete the following resultMask case in favor of performance because val is constant and users use nodata mask, otherwise nodatavalue post processing handles it too.
+ //while the above statement is true, we're not doing it as we want to keep invalid pixel value at 0 rather than arbitrary values
+ if (mask) {
+ for (row = 0; row < thisBlockHeight; row++) {
+ for (col = 0; col < thisBlockWidth; col++) {
+ if (mask[outPtr]) {
+ resultPixels[outPtr] = isDiffEncoding ? Math.min(zMax, resultPixelsPrevDim[outPtr] + offset) : offset;
+ }
+ outPtr++;
+ }
+ outPtr += outStride;
+ }
+ }
+ else {
+ for (row = 0; row < thisBlockHeight; row++) {
+ for (col = 0; col < thisBlockWidth; col++) {
+ resultPixels[outPtr] = isDiffEncoding ? Math.min(zMax, resultPixelsPrevDim[outPtr] + offset) : offset;
+ outPtr++;
+ }
+ outPtr += outStride;
+ }
+ }
+ }
+ else { //bitstuff encoding is 3
+ data.ptr += blockPtr;
+ //heavy lifting
+ Lerc2Helpers.decodeBits(input, data, blockDataBuffer, offset, iDim);
+ blockPtr = 0;
+ // duplicate code to favor performance, diff encoding is for multidimension only
+ if (isDiffEncoding) {
+ if (mask) {
+ for (row = 0; row < thisBlockHeight; row++) {
+ for (col = 0; col < thisBlockWidth; col++) {
+ if (mask[outPtr]) {
+ resultPixels[outPtr] = blockDataBuffer[blockPtr++] + resultPixelsPrevDim[outPtr];
+ }
+ outPtr++;
+ }
+ outPtr += outStride;
+ }
+ }
+ else {
+ for (row = 0; row < thisBlockHeight; row++) {
+ for (col = 0; col < thisBlockWidth; col++) {
+ resultPixels[outPtr] = blockDataBuffer[blockPtr++] + resultPixelsPrevDim[outPtr];
+ outPtr++;
+ }
+ outPtr += outStride;
+ }
+ }
+ }
+ else if (mask) {
+ for (row = 0; row < thisBlockHeight; row++) {
+ for (col = 0; col < thisBlockWidth; col++) {
+ if (mask[outPtr]) {
+ resultPixels[outPtr] = blockDataBuffer[blockPtr++];
+ }
+ outPtr++;
+ }
+ outPtr += outStride;
+ }
+ }
+ else {
+ for (row = 0; row < thisBlockHeight; row++) {
+ for (col = 0; col < thisBlockWidth; col++) {
+ resultPixels[outPtr++] = blockDataBuffer[blockPtr++];
+ }
+ outPtr += outStride;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+
+ /*****************
+ * private methods (helper methods)
+ *****************/
+
+ formatFileInfo: function (data) {
+ return {
+ "fileIdentifierString": data.headerInfo.fileIdentifierString,
+ "fileVersion": data.headerInfo.fileVersion,
+ "imageType": data.headerInfo.imageType,
+ "height": data.headerInfo.height,
+ "width": data.headerInfo.width,
+ "numValidPixel": data.headerInfo.numValidPixel,
+ "microBlockSize": data.headerInfo.microBlockSize,
+ "blobSize": data.headerInfo.blobSize,
+ "maxZError": data.headerInfo.maxZError,
+ "pixelType": Lerc2Helpers.getPixelType(data.headerInfo.imageType),
+ "eofOffset": data.eofOffset,
+ "mask": data.mask ? {
+ "numBytes": data.mask.numBytes
+ } : null,
+ "pixels": {
+ "numBlocksX": data.pixels.numBlocksX,
+ "numBlocksY": data.pixels.numBlocksY,
+ //"numBytes": data.pixels.numBytes,
+ "maxValue": data.headerInfo.zMax,
+ "minValue": data.headerInfo.zMin,
+ "noDataValue": data.noDataValue
+ }
+ };
+ },
+
+ constructConstantSurface: function (data) {
+ var val = data.headerInfo.zMax;
+ var numDims = data.headerInfo.numDims;
+ var numPixels = data.headerInfo.height * data.headerInfo.width;
+ var i = 0, k = 0, nStart = 0;
+ var mask = data.pixels.resultMask;
+ var resultPixels = data.pixels.resultPixels;
+ if (mask) {
+ if (numDims > 1) {
+ for (i = 0; i < numDims; i++) {
+ nStart = i * numPixels;
+ val = data.headerInfo.maxValues[i];
+ for (k = 0; k < numPixels; k++) {
+ if (mask[k]) {
+ resultPixels[nStart + k] = val;
+ }
+ }
+ }
+ }
+ else {
+ for (k = 0; k < numPixels; k++) {
+ if (mask[k]) {
+ resultPixels[k] = val;
+ }
+ }
+ }
+ }
+ else {
+ if (numDims > 1) {
+ for (i = 0; i < numDims; i++) {
+ nStart = i * numPixels;
+ val = data.headerInfo.maxValues[i];
+ for (k = 0; k < numPixels; k++) {
+ resultPixels[nStart + k] = val;
+ }
+ }
+ }
+ else {
+ for (k = 0; k < numPixels; k++) {
+ resultPixels[k] = val;
+ }
+ }
+ }
+ return;
+ },
+
+ getDataTypeArray: function (t) {
+ var tp;
+ switch (t) {
+ case 0: //char
+ tp = Int8Array;
+ break;
+ case 1: //byte
+ tp = Uint8Array;
+ break;
+ case 2: //short
+ tp = Int16Array;
+ break;
+ case 3: //ushort
+ tp = Uint16Array;
+ break;
+ case 4:
+ tp = Int32Array;
+ break;
+ case 5:
+ tp = Uint32Array;
+ break;
+ case 6:
+ tp = Float32Array;
+ break;
+ case 7:
+ tp = Float64Array;
+ break;
+ default:
+ tp = Float32Array;
+ }
+ return tp;
+ },
+
+ getPixelType: function (t) {
+ var tp;
+ switch (t) {
+ case 0: //char
+ tp = "S8";
+ break;
+ case 1: //byte
+ tp = "U8";
+ break;
+ case 2: //short
+ tp = "S16";
+ break;
+ case 3: //ushort
+ tp = "U16";
+ break;
+ case 4:
+ tp = "S32";
+ break;
+ case 5:
+ tp = "U32";
+ break;
+ case 6:
+ tp = "F32";
+ break;
+ case 7:
+ tp = "F64"; //not supported
+ break;
+ default:
+ tp = "F32";
+ }
+ return tp;
+ },
+
+ isValidPixelValue: function (t, val) {
+ if (val == null) {
+ return false;
+ }
+ var isValid;
+ switch (t) {
+ case 0: //char
+ isValid = val >= -128 && val <= 127;
+ break;
+ case 1: //byte (unsigned char)
+ isValid = val >= 0 && val <= 255;
+ break;
+ case 2: //short
+ isValid = val >= -32768 && val <= 32767;
+ break;
+ case 3: //ushort
+ isValid = val >= 0 && val <= 65536;
+ break;
+ case 4: //int 32
+ isValid = val >= -2147483648 && val <= 2147483647;
+ break;
+ case 5: //uinit 32
+ isValid = val >= 0 && val <= 4294967296;
+ break;
+ case 6:
+ isValid = val >= -3.4027999387901484e+38 && val <= 3.4027999387901484e+38;
+ break;
+ case 7:
+ isValid = val >= 5e-324 && val <= 1.7976931348623157e+308;
+ break;
+ default:
+ isValid = false;
+ }
+ return isValid;
+ },
+
+ getDataTypeSize: function (t) {
+ var s = 0;
+ switch (t) {
+ case 0: //ubyte
+ case 1: //byte
+ s = 1;
+ break;
+ case 2: //short
+ case 3: //ushort
+ s = 2;
+ break;
+ case 4:
+ case 5:
+ case 6:
+ s = 4;
+ break;
+ case 7:
+ s = 8;
+ break;
+ default:
+ s = t;
+ }
+ return s;
+ },
+
+ getDataTypeUsed: function (dt, tc) {
+ var t = dt;
+ switch (dt) {
+ case 2: //short
+ case 4: //long
+ t = dt - tc;
+ break;
+ case 3: //ushort
+ case 5: //ulong
+ t = dt - 2 * tc;
+ break;
+ case 6: //float
+ if (0 === tc) {
+ t = dt;
+ }
+ else if (1 === tc) {
+ t = 2;
+ }
+ else {
+ t = 1;//byte
+ }
+ break;
+ case 7: //double
+ if (0 === tc) {
+ t = dt;
+ }
+ else {
+ t = dt - 2 * tc + 1;
+ }
+ break;
+ default:
+ t = dt;
+ break;
+ }
+ return t;
+ },
+
+ getOnePixel: function (block, blockPtr, offsetType, view) {
+ var temp = 0;
+ switch (offsetType) {
+ case 0: //char
+ temp = view.getInt8(blockPtr);
+ break;
+ case 1: //byte
+ temp = view.getUint8(blockPtr);
+ break;
+ case 2:
+ temp = view.getInt16(blockPtr, true);
+ break;
+ case 3:
+ temp = view.getUint16(blockPtr, true);
+ break;
+ case 4:
+ temp = view.getInt32(blockPtr, true);
+ break;
+ case 5:
+ temp = view.getUInt32(blockPtr, true);
+ break;
+ case 6:
+ temp = view.getFloat32(blockPtr, true);
+ break;
+ case 7:
+ //temp = view.getFloat64(blockPtr, true);
+ //blockPtr += 8;
+ //lerc2 encoding doesnt handle float 64, force to float32???
+ temp = view.getFloat64(blockPtr, true);
+ break;
+ default:
+ throw "the decoder does not understand this pixel type";
+ }
+ return temp;
+ }
+ };
+
+ /***************************************************
+ *private class for a tree node. Huffman code is in Lerc2Helpers
+ ****************************************************/
+ var TreeNode = function (val, left, right) {
+ this.val = val;
+ this.left = left;
+ this.right = right;
+ };
+
+ var Lerc2Decode = {
+ /*
+ * ********removed options compared to LERC1. We can bring some of them back if needed.
+ * removed pixel type. LERC2 is typed and doesn't require user to give pixel type
+ * changed encodedMaskData to maskData. LERC2 's js version make it faster to use maskData directly.
+ * removed returnMask. mask is used by LERC2 internally and is cost free. In case of user input mask, it's returned as well and has neglible cost.
+ * removed nodatavalue. Because LERC2 pixels are typed, nodatavalue will sacrify a useful value for many types (8bit, 16bit) etc,
+ * user has to be knowledgable enough about raster and their data to avoid usability issues. so nodata value is simply removed now.
+ * We can add it back later if their's a clear requirement.
+ * removed encodedMask. This option was not implemented in LercDecode. It can be done after decoding (less efficient)
+ * removed computeUsedBitDepths.
+ *
+ *
+ * response changes compared to LERC1
+ * 1. encodedMaskData is not available
+ * 2. noDataValue is optional (returns only if user's noDataValue is with in the valid data type range)
+ * 3. maskData is always available
+ */
+ /*****************
+ * public properties
+ ******************/
+ //HUFFMAN_LUT_BITS_MAX: 12, //use 2^12 lut, not configurable
+
+ /*****************
+ * public methods
+ *****************/
+
+ /**
+ * Decode a LERC2 byte stream and return an object containing the pixel data and optional metadata.
+ *
+ * @param {ArrayBuffer} input The LERC input byte stream
+ * @param {object} [options] options Decoding options
+ * @param {number} [options.inputOffset] The number of bytes to skip in the input byte stream. A valid LERC file is expected at that position
+ * @param {boolean} [options.returnFileInfo] If true, the return value will have a fileInfo property that contains metadata obtained from the LERC headers and the decoding process
+ */
+ decode: function (/*byte array*/ input, /*object*/ options) {
+ //currently there's a bug in the sparse array, so please do not set to false
+ options = options || {};
+ var noDataValue = options.noDataValue;
+
+ //initialize
+ var i = 0, data = {};
+ data.ptr = options.inputOffset || 0;
+ data.pixels = {};
+
+ // File header
+ if (!Lerc2Helpers.readHeaderInfo(input, data)) {
+ return;
+ }
+
+ var headerInfo = data.headerInfo;
+ var fileVersion = headerInfo.fileVersion;
+ var OutPixelTypeArray = Lerc2Helpers.getDataTypeArray(headerInfo.imageType);
+
+ // version check
+ if (fileVersion > 5) {
+ throw "unsupported lerc version 2." + fileVersion;
+ }
+
+ // Mask Header
+ Lerc2Helpers.readMask(input, data);
+ if (headerInfo.numValidPixel !== headerInfo.width * headerInfo.height && !data.pixels.resultMask) {
+ data.pixels.resultMask = options.maskData;
+ }
+
+ var numPixels = headerInfo.width * headerInfo.height;
+ data.pixels.resultPixels = new OutPixelTypeArray(numPixels * headerInfo.numDims);
+
+ data.counter = {
+ onesweep: 0,
+ uncompressed: 0,
+ lut: 0,
+ bitstuffer: 0,
+ constant: 0,
+ constantoffset: 0
+ };
+ if (headerInfo.numValidPixel !== 0) {
+ //not tested
+ if (headerInfo.zMax === headerInfo.zMin) //constant surface
+ {
+ Lerc2Helpers.constructConstantSurface(data);
+ }
+ else if (fileVersion >= 4 && Lerc2Helpers.checkMinMaxRanges(input, data)) {
+ Lerc2Helpers.constructConstantSurface(data);
+ }
+ else {
+ var view = new DataView(input, data.ptr, 2);
+ var bReadDataOneSweep = view.getUint8(0);
+ data.ptr++;
+ if (bReadDataOneSweep) {
+ //console.debug("OneSweep");
+ Lerc2Helpers.readDataOneSweep(input, data, OutPixelTypeArray);
+ }
+ else {
+ //lerc2.1: //bitstuffing + lut
+ //lerc2.2: //bitstuffing + lut + huffman
+ //lerc2.3: new bitstuffer
+ if (fileVersion > 1 && headerInfo.imageType <= 1 && Math.abs(headerInfo.maxZError - 0.5) < 0.00001) {
+ //this is 2.x plus 8 bit (unsigned and signed) data, possiblity of Huffman
+ var flagHuffman = view.getUint8(1);
+ data.ptr++;
+ data.encodeMode = flagHuffman;
+ if (flagHuffman > 2 || fileVersion < 4 && flagHuffman > 1) {
+ throw "Invalid Huffman flag " + flagHuffman;
+ }
+ if (flagHuffman) {//1 - delta Huffman, 2 - Huffman
+ //console.log("Huffman");
+ Lerc2Helpers.readHuffman(input, data, OutPixelTypeArray);
+ }
+ else {
+ //console.log("Tiles");
+ Lerc2Helpers.readTiles(input, data, OutPixelTypeArray);
+ }
+ }
+ else { //lerc2.x non-8 bit data
+ //console.log("Tiles");
+ Lerc2Helpers.readTiles(input, data, OutPixelTypeArray);
+ }
+ }
+ }
+ }
+
+ data.eofOffset = data.ptr;
+ var diff;
+ if (options.inputOffset) {
+ diff = data.headerInfo.blobSize + options.inputOffset - data.ptr;
+ if (Math.abs(diff) >= 1) {
+ //console.debug("incorrect eof: dataptr " + data.ptr + " offset " + options.inputOffset + " blobsize " + data.headerInfo.blobSize + " diff: " + diff);
+ data.eofOffset = options.inputOffset + data.headerInfo.blobSize;
+ }
+ }
+ else {
+ diff = data.headerInfo.blobSize - data.ptr;
+ if (Math.abs(diff) >= 1) {
+ //console.debug("incorrect first band eof: dataptr " + data.ptr + " blobsize " + data.headerInfo.blobSize + " diff: " + diff);
+ data.eofOffset = data.headerInfo.blobSize;
+ }
+ }
+
+ var result = {
+ width: headerInfo.width,
+ height: headerInfo.height,
+ pixelData: data.pixels.resultPixels,
+ minValue: headerInfo.zMin,
+ maxValue: headerInfo.zMax,
+ validPixelCount: headerInfo.numValidPixel,
+ dimCount: headerInfo.numDims,
+ dimStats: {
+ minValues: headerInfo.minValues,
+ maxValues: headerInfo.maxValues
+ },
+ maskData: data.pixels.resultMask
+ //noDataValue: noDataValue
+ };
+
+ //we should remove this if there's no existing client
+ //optional noDataValue processing, it's user's responsiblity
+ if (data.pixels.resultMask && Lerc2Helpers.isValidPixelValue(headerInfo.imageType, noDataValue)) {
+ var mask = data.pixels.resultMask;
+ for (i = 0; i < numPixels; i++) {
+ if (!mask[i]) {
+ result.pixelData[i] = noDataValue;
+ }
+ }
+ result.noDataValue = noDataValue;
+ }
+ data.noDataValue = noDataValue;
+ if (options.returnFileInfo) {
+ result.fileInfo = Lerc2Helpers.formatFileInfo(data);
+ }
+ return result;
+ },
+
+ getBandCount: function (/*byte array*/ input) {
+ var count = 0;
+ var i = 0;
+ var temp = {};
+ temp.ptr = 0;
+ temp.pixels = {};
+ while (i < input.byteLength - 58) {
+ Lerc2Helpers.readHeaderInfo(input, temp);
+ i += temp.headerInfo.blobSize;
+ count++;
+ temp.ptr = i;
+ }
+ return count;
+ }
+ };
+
+ return Lerc2Decode;
+})();
+
+var isPlatformLittleEndian = (function () {
+ var a = new ArrayBuffer(4);
+ var b = new Uint8Array(a);
+ var c = new Uint32Array(a);
+ c[0] = 1;
+ return b[0] === 1;
+})();
+
+var Lerc = {
+ /************wrapper**********************************************/
+ /**
+ * A wrapper for decoding both LERC1 and LERC2 byte streams capable of handling multiband pixel blocks for various pixel types.
+ *
+ * @alias module:Lerc
+ * @param {ArrayBuffer} input The LERC input byte stream
+ * @param {object} [options] The decoding options below are optional.
+ * @param {number} [options.inputOffset] The number of bytes to skip in the input byte stream. A valid Lerc file is expected at that position.
+ * @param {string} [options.pixelType] (LERC1 only) Default value is F32. Valid pixel types for input are U8/S8/S16/U16/S32/U32/F32.
+ * @param {number} [options.noDataValue] (LERC1 only). It is recommended to use the returned mask instead of setting this value.
+ * @returns {{width, height, pixels, pixelType, mask, statistics}}
+ * @property {number} width Width of decoded image.
+ * @property {number} height Height of decoded image.
+ * @property {array} pixels [band1, band2, …] Each band is a typed array of width*height.
+ * @property {string} pixelType The type of pixels represented in the output.
+ * @property {mask} mask Typed array with a size of width*height, or null if all pixels are valid.
+ * @property {array} statistics [statistics_band1, statistics_band2, …] Each element is a statistics object representing min and max values
+ **/
+ decode: function (encodedData, options) {
+ if (!isPlatformLittleEndian) {
+ throw "Big endian system is not supported.";
+ }
+ options = options || {};
+ var inputOffset = options.inputOffset || 0;
+ var fileIdView = new Uint8Array(encodedData, inputOffset, 10);
+ var fileIdentifierString = String.fromCharCode.apply(null, fileIdView);
+ var lerc, majorVersion;
+ if (fileIdentifierString.trim() === "CntZImage") {
+ lerc = LercDecode;
+ majorVersion = 1;
+ }
+ else if (fileIdentifierString.substring(0, 5) === "Lerc2") {
+ lerc = Lerc2Decode;
+ majorVersion = 2;
+ }
+ else {
+ throw "Unexpected file identifier string: " + fileIdentifierString;
+ }
+
+ var iPlane = 0, eof = encodedData.byteLength - 10, encodedMaskData, bandMasks = [], bandMask, maskData;
+ var decodedPixelBlock = {
+ width: 0,
+ height: 0,
+ pixels: [],
+ pixelType: options.pixelType,
+ mask: null,
+ statistics: []
+ };
+
+ while (inputOffset < eof) {
+ var result = lerc.decode(encodedData, {
+ inputOffset: inputOffset,//for both lerc1 and lerc2
+ encodedMaskData: encodedMaskData,//lerc1 only
+ maskData: maskData,//lerc2 only
+ returnMask: iPlane === 0 ? true : false,//lerc1 only
+ returnEncodedMask: iPlane === 0 ? true : false,//lerc1 only
+ returnFileInfo: true,//for both lerc1 and lerc2
+ pixelType: options.pixelType || null,//lerc1 only
+ noDataValue: options.noDataValue || null//lerc1 only
+ });
+
+ inputOffset = result.fileInfo.eofOffset;
+ if (iPlane === 0) {
+ encodedMaskData = result.encodedMaskData;//lerc1
+ maskData = result.maskData;//lerc2
+ decodedPixelBlock.width = result.width;
+ decodedPixelBlock.height = result.height;
+ decodedPixelBlock.dimCount = result.dimCount || 1;
+ //decodedPixelBlock.dimStats = decodedPixelBlock.dimStats;
+ decodedPixelBlock.pixelType = result.pixelType || result.fileInfo.pixelType;
+ decodedPixelBlock.mask = result.maskData;
+ }
+ if (majorVersion > 1 && result.fileInfo.mask && result.fileInfo.mask.numBytes > 0) {
+ bandMasks.push(result.maskData);
+ }
+
+ iPlane++;
+ decodedPixelBlock.pixels.push(result.pixelData);
+ decodedPixelBlock.statistics.push({
+ minValue: result.minValue,
+ maxValue: result.maxValue,
+ noDataValue: result.noDataValue,
+ dimStats: result.dimStats
+ });
+ }
+ var i, j, numPixels;
+ if (majorVersion > 1 && bandMasks.length > 1) {
+ numPixels = decodedPixelBlock.width * decodedPixelBlock.height;
+ decodedPixelBlock.bandMasks = bandMasks;
+ maskData = new Uint8Array(numPixels);
+ maskData.set(bandMasks[0]);
+ for (i = 1; i < bandMasks.length; i++) {
+ bandMask = bandMasks[i];
+ for (j = 0; j < numPixels; j++) {
+ maskData[j] = maskData[j] & bandMask[j];
+ }
+ }
+ decodedPixelBlock.maskData = maskData;
+ }
+
+ return decodedPixelBlock;
+ }
+};
+
+// Download and translate arcgis terrain data.
+self.onmessage = e => {
+ let { tileKey, x, y, z } = e.data;
+
+ let xhr = new XMLHttpRequest();
+ let url = `http://localhost:2020/api/Map/Elevation/tile/${z}/${y}/${x}`;
+
+ xhr.open("GET", url, true);
+ xhr.responseType = 'arraybuffer';
+ xhr.onreadystatechange = function () {
+ if (xhr.readyState === 4) {
+ if (xhr.status === 200) {
+ self.postMessage({
+ result: 'success',
+ tileKey,
+ url,
+ data: LercDecode.decode(xhr.response)
+ });
+ } else {
+ self.postMessage({
+ result: 'fail',
+ msg: xhr.statusText,
+ tileKey,
+ url
+ });
+ }
+ }
+ };
+
+ xhr.onerror = function () {
+ self.postMessage({
+ result: 'error',
+ tileKey,
+ url
+ });
+ };
+
+ xhr.ontimeout = function () {
+ self.postMessage({
+ result: 'timeout',
+ tileKey,
+ url
+ });
+ };
+
+ xhr.send(null);
+};
\ No newline at end of file
diff --git a/web/test/WebWorldWind/src/globe/AsterV2ElevationCoverage.js b/web/test/WebWorldWind/src/globe/AsterV2ElevationCoverage.js
new file mode 100644
index 00000000..0082bd98
--- /dev/null
+++ b/web/test/WebWorldWind/src/globe/AsterV2ElevationCoverage.js
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports AsterV2ElevationCoverage
+ */
+import Sector from '../geom/Sector';
+import TiledElevationCoverage from '../globe/TiledElevationCoverage';
+import WmsUrlBuilder from '../util/WmsUrlBuilder';
+
+
+/**
+ * Constructs an Earth elevation coverage using ASTER V2 data.
+ * @alias AsterV2ElevationCoverage
+ * @constructor
+ * @augments TiledElevationCoverage
+ * @classdesc Provides elevations for Earth. Elevations are drawn from the NASA WorldWind elevation service.
+ */
+function AsterV2ElevationCoverage() {
+ TiledElevationCoverage.call(this, {
+ coverageSector: new Sector(-83.0001, 83.0001, -180, 180),
+ resolution: 0.000277777777778,
+ retrievalImageFormat: "application/bil16",
+ minElevation: -11000,
+ maxElevation: 8850,
+ urlBuilder: new WmsUrlBuilder("https://worldwind26.arc.nasa.gov/elev", "aster_v2", "", "1.3.0")
+ });
+
+ this.displayName = "ASTER V2 Earth Elevation Coverage";
+}
+
+AsterV2ElevationCoverage.prototype = Object.create(TiledElevationCoverage.prototype);
+
+export default AsterV2ElevationCoverage;
diff --git a/web/test/WebWorldWind/src/globe/EarthElevationModel.js b/web/test/WebWorldWind/src/globe/EarthElevationModel.js
new file mode 100644
index 00000000..ebc5378d
--- /dev/null
+++ b/web/test/WebWorldWind/src/globe/EarthElevationModel.js
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports EarthElevationModel
+ */
+import AsterV2ElevationCoverage from '../globe/AsterV2ElevationCoverage';
+import ElevationModel from '../globe/ElevationModel';
+import GebcoElevationCoverage from '../globe/GebcoElevationCoverage';
+import UsgsNedElevationCoverage from '../globe/UsgsNedElevationCoverage';
+import UsgsNedHiElevationCoverage from '../globe/UsgsNedHiElevationCoverage';
+import ArcgisElevationCoverage from './ArcgisElevationCoverage';
+
+/**
+ * Constructs an EarthElevationModel consisting of three elevation coverages GEBCO, Aster V2, and USGS NED.
+ * @alias EarthElevationModel
+ * @constructor
+ */
+function EarthElevationModel() {
+ ElevationModel.call(this);
+
+ this.addCoverage(new ArcgisElevationCoverage());
+ // this.addCoverage(new GebcoElevationCoverage());
+ // this.addCoverage(new AsterV2ElevationCoverage());
+ // this.addCoverage(new UsgsNedElevationCoverage());
+ // this.addCoverage(new UsgsNedHiElevationCoverage());
+}
+
+EarthElevationModel.prototype = Object.create(ElevationModel.prototype);
+
+export default EarthElevationModel;
diff --git a/web/test/WebWorldWind/src/globe/ElevationCoverage.js b/web/test/WebWorldWind/src/globe/ElevationCoverage.js
new file mode 100644
index 00000000..c0e93464
--- /dev/null
+++ b/web/test/WebWorldWind/src/globe/ElevationCoverage.js
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports ElevationCoverage
+ */
+import ArgumentError from '../error/ArgumentError';
+import Logger from '../util/Logger';
+import Sector from '../geom/Sector';
+
+
+/**
+ * Constructs an ElevationCoverage
+ * @alias ElevationCoverage
+ * @constructor
+ * @classdesc When used directly and not through a subclass, this class represents an elevation coverage
+ * whose elevations are zero at all locations.
+ * @param {Number} resolution The resolution of the coverage, in degrees. (To compute degrees from
+ * meters, divide the number of meters by the globe's radius to obtain radians and convert the result to degrees.)
+ * @throws {ArgumentError} If the resolution argument is null, undefined, or zero.
+ */
+function ElevationCoverage(resolution) {
+ if (!resolution) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationCoverage", "constructor",
+ "missingResolution"));
+ }
+
+ /**
+ * Indicates the last time this coverage changed, in milliseconds since midnight Jan 1, 1970.
+ * @type {Number}
+ * @readonly
+ * @default Date.now() at construction
+ */
+ this.timestamp = Date.now();
+
+ /**
+ * Indicates this coverage's display name.
+ * @type {String}
+ * @default "Coverage"
+ */
+ this.displayName = "Coverage";
+
+ /**
+ * Indicates whether or not to use this coverage.
+ * @type {Boolean}
+ * @default true
+ */
+ this._enabled = true;
+
+ /**
+ * The resolution of this coverage in degrees.
+ * @type {Number}
+ */
+ this.resolution = resolution;
+
+ /**
+ * The sector this coverage spans.
+ * @type {Sector}
+ * @readonly
+ */
+ this.coverageSector = Sector.FULL_SPHERE;
+}
+
+Object.defineProperties(ElevationCoverage.prototype, {
+ /**
+ * Indicates whether or not to use this coverage.
+ * @type {Boolean}
+ * @default true
+ */
+ enabled: {
+ get: function () {
+ return this._enabled;
+ },
+ set: function (value) {
+ this._enabled = value;
+ this.timestamp = Date.now();
+ }
+ }
+});
+
+/**
+ * Returns the minimum and maximum elevations within a specified sector.
+ * @param {Sector} sector The sector for which to determine extreme elevations.
+ * @param {Number[]} result An array in which to return the requested minimum and maximum elevations.
+ * @returns {Boolean} true if the coverage completely fills the sector with data, false otherwise.
+ * @throws {ArgumentError} If any argument is null or undefined
+ */
+ElevationCoverage.prototype.minAndMaxElevationsForSector = function (sector, result) {
+ if (!sector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationCoverage", "minAndMaxElevationsForSector", "missingSector"));
+ }
+
+ if (!result) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationCoverage", "minAndMaxElevationsForSector", "missingResult"));
+ }
+
+ if (result[0] > 0) { // min elevation
+ result[0] = 0;
+ }
+
+ if (result[1] < 0) { // max elevation
+ result[1] = 0;
+ }
+
+ return true;
+};
+
+/**
+ * Returns the elevation at a specified location.
+ * @param {Number} latitude The location's latitude in degrees.
+ * @param {Number} longitude The location's longitude in degrees.
+ * @returns {Number} The elevation at the specified location, in meters. Returns null if the location is
+ * outside the coverage area of this coverage.
+ */
+ElevationCoverage.prototype.elevationAtLocation = function (latitude, longitude) {
+ return 0;
+};
+
+/**
+ * Returns the elevations at locations within a specified sector.
+ * @param {Sector} sector The sector for which to determine the elevations.
+ * @param {Number} numLat The number of latitudinal sample locations within the sector.
+ * @param {Number} numLon The number of longitudinal sample locations within the sector.
+ * @param {Number[]} result An array in which to return the requested elevations.
+ * @returns {Boolean} true if the result array was completely filled with elevation data, false otherwise.
+ * @throws {ArgumentError} If the specified sector or result array is null or undefined, or if either of the
+ * specified numLat or numLon values is less than one.
+ */
+ElevationCoverage.prototype.elevationsForGrid = function (sector, numLat, numLon, result) {
+ if (!sector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationCoverage", "elevationsForGrid", "missingSector"));
+ }
+
+ if (numLat <= 0 || numLon <= 0) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationCoverage",
+ "elevationsForGrid", "numLat or numLon is less than 1"));
+ }
+
+ if (!result || result.length < numLat * numLon) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationCoverage",
+ "elevationsForGrid", "missingArray"));
+ }
+
+ for (var i = 0, len = result.length; i < len; i++) {
+ result[i] = 0;
+ }
+
+ return true;
+};
+
+export default ElevationCoverage;
diff --git a/web/test/WebWorldWind/src/globe/ElevationImage.js b/web/test/WebWorldWind/src/globe/ElevationImage.js
new file mode 100644
index 00000000..3c22eadf
--- /dev/null
+++ b/web/test/WebWorldWind/src/globe/ElevationImage.js
@@ -0,0 +1,354 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports ElevationImage
+ */
+import ArgumentError from '../error/ArgumentError';
+import Logger from '../util/Logger';
+import WWMath from '../util/WWMath';
+
+
+/**
+ * Constructs an elevation image.
+ * @alias ElevationImage
+ * @constructor
+ * @classdesc Holds elevation values for an elevation tile.
+ * This class is typically not used directly by applications.
+ * @param {Sector} sector The sector spanned by this elevation image.
+ * @param {Number} imageWidth The number of longitudinal sample points in this elevation image.
+ * @param {Number} imageHeight The number of latitudinal sample points in this elevation image.
+ * @throws {ArgumentError} If the sector is null or undefined
+ */
+function ElevationImage(sector, imageWidth, imageHeight) {
+ if (!sector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationImage", "constructor", "missingSector"));
+ }
+
+ /**
+ * The sector spanned by this elevation image.
+ * @type {Sector}
+ * @readonly
+ */
+ this.sector = sector;
+
+ /**
+ * The number of longitudinal sample points in this elevation image.
+ * @type {Number}
+ * @readonly
+ */
+ this.imageWidth = imageWidth;
+
+ /**
+ * The number of latitudinal sample points in this elevation image.
+ * @type {Number}
+ * @readonly
+ */
+ this.imageHeight = imageHeight;
+
+ /**
+ * The size in bytes of this elevation image.
+ * @type {number}
+ * @readonly
+ */
+ this.size = this.imageWidth * this.imageHeight;
+
+ /**
+ * Internal use only
+ * false if the entire image consists of NO_DATA values, true otherwise.
+ * @ignore
+ */
+ this.hasData = true;
+}
+
+/**
+ * Internal use only
+ * The value that indicates a pixel contains no data.
+ * TODO: This will eventually need to become an instance property
+ * @ignore
+ */
+ElevationImage.NO_DATA = 0;
+
+/**
+ * Internal use only
+ * Returns true if a set of elevation pixels represents the NO_DATA value.
+ * @ignore
+ */
+ElevationImage.isNoData = function (x0y0, x1y0, x0y1, x1y1) {
+ // TODO: Change this logic once proper NO_DATA value handling is in place.
+ var v = ElevationImage.NO_DATA;
+ return x0y0 === v &&
+ x1y0 === v &&
+ x0y1 === v &&
+ x1y1 === v;
+};
+
+/**
+ * Returns the pixel value at a specified coordinate in this elevation image. The coordinate origin is the
+ * image's lower left corner, so (0, 0) indicates the lower left pixel and (imageWidth-1, imageHeight-1)
+ * indicates the upper right pixel. This returns 0 if the coordinate indicates a pixel outside of this elevation
+ * image.
+ * @param x The pixel's X coordinate.
+ * @param y The pixel's Y coordinate.
+ * @returns {Number} The pixel value at the specified coordinate in this elevation image.
+ * Returns 0 if the coordinate indicates a pixel outside of this elevation image.
+ */
+ElevationImage.prototype.pixel = function (x, y) {
+ debugger;
+ if (x < 0 || x >= this.imageWidth) {
+ return 0;
+ }
+
+ if (y < 0 || y >= this.imageHeight) {
+ return 0;
+ }
+
+ y = this.imageHeight - y - 1; // flip the y coordinate origin to the lower left corner
+ return this.imageData[x + y * this.imageWidth];
+};
+
+/**
+ * Returns the elevation at a specified geographic location.
+ * @param {Number} latitude The location's latitude.
+ * @param {Number} longitude The location's longitude.
+ * @returns {Number} The elevation at the specified location.
+ */
+ElevationImage.prototype.elevationAtLocation = function (latitude, longitude) {
+ var maxLat = this.sector.maxLatitude,
+ minLon = this.sector.minLongitude,
+ deltaLat = this.sector.deltaLatitude(),
+ deltaLon = this.sector.deltaLongitude(),
+ x = (this.imageWidth - 1) * (longitude - minLon) / deltaLon,
+ y = (this.imageHeight - 1) * (maxLat - latitude) / deltaLat,
+ x0 = Math.floor(WWMath.clamp(x, 0, this.imageWidth - 1)),
+ x1 = Math.floor(WWMath.clamp(x0 + 1, 0, this.imageWidth - 1)),
+ y0 = Math.floor(WWMath.clamp(y, 0, this.imageHeight - 1)),
+ y1 = Math.floor(WWMath.clamp(y0 + 1, 0, this.imageHeight - 1)),
+ pixels = this.imageData,
+ x0y0 = pixels[x0 + y0 * this.imageWidth],
+ x1y0 = pixels[x1 + y0 * this.imageWidth],
+ x0y1 = pixels[x0 + y1 * this.imageWidth],
+ x1y1 = pixels[x1 + y1 * this.imageWidth],
+ xf = x - x0,
+ yf = y - y0;
+
+ if (ElevationImage.isNoData(x0y0, x1y0, x0y1, x1y1)) {
+ return NaN;
+ }
+
+ return (1 - xf) * (1 - yf) * x0y0 +
+ xf * (1 - yf) * x1y0 +
+ (1 - xf) * yf * x0y1 +
+ xf * yf * x1y1;
+};
+
+/**
+ * Returns elevations for a specified sector.
+ * @param {Sector} sector The sector for which to return the elevations.
+ * @param {Number} numLat The number of sample points in the longitudinal direction.
+ * @param {Number} numLon The number of sample points in the latitudinal direction.
+ * @param {Number[]} result An array in which to return the computed elevations.
+ * @throws {ArgumentError} If either the specified sector or result argument is null or undefined, or if the
+ * specified number of sample points in either direction is less than 1.
+ */
+ElevationImage.prototype.elevationsForGrid = function (sector, numLat, numLon, result) {
+ if (!sector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationImage", "elevationsForGrid", "missingSector"));
+ }
+
+ if (numLat < 1 || numLon < 1) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationImage", "elevationsForGrid",
+ "The specified number of sample points is less than 1."));
+ }
+
+ if (!result) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationImage", "elevationsForGrid", "missingResult"));
+ }
+
+ var minLatSelf = this.sector.minLatitude,
+ maxLatSelf = this.sector.maxLatitude,
+ minLonSelf = this.sector.minLongitude,
+ maxLonSelf = this.sector.maxLongitude,
+ deltaLatSelf = maxLatSelf - minLatSelf,
+ deltaLonSelf = maxLonSelf - minLonSelf,
+ minLat = sector.minLatitude,
+ maxLat = sector.maxLatitude,
+ minLon = sector.minLongitude,
+ maxLon = sector.maxLongitude,
+ deltaLat = (maxLat - minLat) / (numLat > 1 ? numLat - 1 : 1),
+ deltaLon = (maxLon - minLon) / (numLon > 1 ? numLon - 1 : 1),
+ lat, lon,
+ i, j, index = 0,
+ pixels = this.imageData;
+
+ for (j = 0, lat = minLat; j < numLat; j += 1, lat += deltaLat) {
+ if (j === numLat - 1) {
+ lat = maxLat; // explicitly set the last lat to the max latitude to ensure alignment
+ }
+
+ if (lat >= minLatSelf && lat <= maxLatSelf) {
+ // Image y-coordinate of the specified location, given an image origin in the top-left corner.
+ var y = (this.imageHeight - 1) * (maxLatSelf - lat) / deltaLatSelf,
+ y0 = Math.floor(WWMath.clamp(y, 0, this.imageHeight - 1)),
+ y1 = Math.floor(WWMath.clamp(y0 + 1, 0, this.imageHeight - 1)),
+ yf = y - y0;
+
+ for (i = 0, lon = minLon; i < numLon; i += 1, lon += deltaLon) {
+ if (i === numLon - 1) {
+ lon = maxLon; // explicitly set the last lon to the max longitude to ensure alignment
+ }
+
+ if (lon >= minLonSelf && lon <= maxLonSelf && isNaN(result[index])) {
+ // Image x-coordinate of the specified location, given an image origin in the top-left corner.
+ var x = (this.imageWidth - 1) * (lon - minLonSelf) / deltaLonSelf,
+ x0 = Math.floor(WWMath.clamp(x, 0, this.imageWidth - 1)),
+ x1 = Math.floor(WWMath.clamp(x0 + 1, 0, this.imageWidth - 1)),
+ xf = x - x0;
+
+ var x0y0 = pixels[x0 + y0 * this.imageWidth],
+ x1y0 = pixels[x1 + y0 * this.imageWidth],
+ x0y1 = pixels[x0 + y1 * this.imageWidth],
+ x1y1 = pixels[x1 + y1 * this.imageWidth];
+
+ if (ElevationImage.isNoData(x0y0, x1y0, x0y1, x1y1)) {
+ result[index] = NaN;
+ }
+ else {
+ result[index] = (1 - xf) * (1 - yf) * x0y0 +
+ xf * (1 - yf) * x1y0 +
+ (1 - xf) * yf * x0y1 +
+ xf * yf * x1y1;
+ }
+ }
+
+ index++;
+ }
+ } else {
+ index += numLon; // skip this row
+ }
+ }
+};
+
+/**
+ * Returns the minimum and maximum elevations within a specified sector.
+ * @param {Sector} sector The sector of interest. If null or undefined, the minimum and maximum elevations
+ * for the sector associated with this tile are returned.
+ * @returns {Number[]} An array containing the minimum and maximum elevations within the specified sector,
+ * or null if the specified sector does not include this elevation image's coverage sector or the image is filled with
+ * NO_DATA values.
+ */
+ElevationImage.prototype.minAndMaxElevationsForSector = function (sector) {
+ debugger;
+ if (!this.hasData) {
+ return null;
+ }
+
+ var result = [];
+ if (!sector) { // the sector is this sector
+ result[0] = this.minElevation;
+ result[1] = this.maxElevation;
+ } else if (sector.contains(this.sector)) { // The specified sector completely contains this image; return the image min and max.
+ if (result[0] > this.minElevation) {
+ result[0] = this.minElevation;
+ }
+
+ if (result[1] < this.maxElevation) {
+ result[1] = this.maxElevation;
+ }
+ } else { // The specified sector intersects a portion of this image; compute the min and max from intersecting pixels.
+ var maxLatSelf = this.sector.maxLatitude,
+ minLonSelf = this.sector.minLongitude,
+ deltaLatSelf = this.sector.deltaLatitude(),
+ deltaLonSelf = this.sector.deltaLongitude(),
+ minLatOther = sector.minLatitude,
+ maxLatOther = sector.maxLatitude,
+ minLonOther = sector.minLongitude,
+ maxLonOther = sector.maxLongitude;
+
+ // Image coordinates of the specified sector, given an image origin in the top-left corner. We take the floor and
+ // ceiling of the min and max coordinates, respectively, in order to capture all pixels that would contribute to
+ // elevations computed for the specified sector in a call to elevationsForSector.
+ var minY = Math.floor((this.imageHeight - 1) * (maxLatSelf - maxLatOther) / deltaLatSelf),
+ maxY = Math.ceil((this.imageHeight - 1) * (maxLatSelf - minLatOther) / deltaLatSelf),
+ minX = Math.floor((this.imageWidth - 1) * (minLonOther - minLonSelf) / deltaLonSelf),
+ maxX = Math.ceil((this.imageWidth - 1) * (maxLonOther - minLonSelf) / deltaLonSelf);
+
+ minY = WWMath.clamp(minY, 0, this.imageHeight - 1);
+ maxY = WWMath.clamp(maxY, 0, this.imageHeight - 1);
+ minX = WWMath.clamp(minX, 0, this.imageWidth - 1);
+ maxX = WWMath.clamp(maxX, 0, this.imageWidth - 1);
+
+ var pixels = this.imageData,
+ min = Number.MAX_VALUE,
+ max = -min;
+
+ for (var y = minY; y <= maxY; y++) {
+ for (var x = minX; x <= maxX; x++) {
+ var p = pixels[Math.floor(x + y * this.imageWidth)];
+ if (min > p) {
+ min = p;
+ }
+
+ if (max < p) {
+ max = p;
+ }
+ }
+ }
+
+ if (result[0] > min) {
+ result[0] = min;
+ }
+
+ if (result[1] < max) {
+ result[1] = max;
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Determines the minimum and maximum elevations within this elevation image and stores those values within
+ * this object. See [minAndMaxElevationsForSector]{@link ElevationImage#minAndMaxElevationsForSector}
+ */
+ElevationImage.prototype.findMinAndMaxElevation = function () {
+ this.hasData = false;
+
+ if (this.imageData && this.imageData.length > 0) {
+ this.hasData = true;
+ this.minElevation = Number.MAX_VALUE;
+ this.maxElevation = -Number.MAX_VALUE;
+
+ var pixels = this.imageData,
+ pixelCount = this.imageWidth * this.imageHeight;
+
+ for (var i = 0; i < pixelCount; i++) {
+ var p = pixels[i];
+ if (this.minElevation > p) {
+ this.minElevation = p;
+ }
+ if (this.maxElevation < p) {
+ this.maxElevation = p;
+ }
+ }
+ }
+};
+
+export default ElevationImage;
diff --git a/web/test/WebWorldWind/src/globe/ElevationModel.js b/web/test/WebWorldWind/src/globe/ElevationModel.js
new file mode 100644
index 00000000..ba938d03
--- /dev/null
+++ b/web/test/WebWorldWind/src/globe/ElevationModel.js
@@ -0,0 +1,417 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports ElevationModel
+ */
+import ArgumentError from '../error/ArgumentError';
+import Location from '../geom/Location';
+import Logger from '../util/Logger';
+
+
+/**
+ * Constructs an elevation model.
+ * @alias ElevationModel
+ * @constructor
+ * @classdesc Represents the elevations for an area, often but not necessarily the whole globe.
+ */
+function ElevationModel() {
+
+ /**
+ * Internal use only
+ * The unique ID of this model.
+ * @type {Array}
+ * @ignore
+ */
+ this.id = 0;
+
+ /**
+ * A string identifying this elevation model's current state. Used to compare states during rendering to
+ * determine whether globe-state dependent cached values must be updated. Applications typically do not
+ * interact with this property. It is primarily used by shapes and terrain generators.
+ * @memberof ElevationModel.prototype
+ * @readonly
+ * @type {String}
+ */
+ this.stateKey = "";
+
+ /**
+ * The list of all elevation coverages usable by this model.
+ * @type {Array}
+ */
+ this.coverages = [];
+
+ this.scratchLocation = new Location(0, 0);
+
+ this.computeStateKey();
+
+}
+
+Object.defineProperties(ElevationModel.prototype, {
+ /**
+ * Indicates the last time the coverages changed, in milliseconds since midnight Jan 1, 1970.
+ * @type {Number}
+ * @readonly
+ */
+ timestamp: {
+ get: function () {
+ var maxTimestamp = 0;
+
+ var i, len;
+ for (i = 0, len = this.coverages.length; i < len; i++) {
+ var coverage = this.coverages[i];
+ if (maxTimestamp < coverage.timestamp) {
+ maxTimestamp = coverage.timestamp;
+ }
+ }
+
+ return maxTimestamp;
+ }
+ },
+
+ /**
+ * This model's minimum elevation in meters across all enabled coverages.
+ * @type {Number}
+ * @readonly
+ */
+ minElevation: {
+ get: function () {
+ var minElevation = Number.MAX_VALUE;
+
+ for (var i = 0, len = this.coverages.length; i < len; i++) {
+ var coverage = this.coverages[i];
+ if (coverage.enabled && coverage.minElevation < minElevation) {
+ minElevation = coverage.minElevation;
+ }
+ }
+
+ return minElevation !== Number.MAX_VALUE ? minElevation : 0; // no coverages or all coverages disabled
+ }
+ },
+
+ /**
+ * This model's maximum elevation in meters across all enabled coverages.
+ * @type {Number}
+ * @readonly
+ */
+ maxElevation: {
+ get: function () {
+ var maxElevation = -Number.MAX_VALUE;
+
+ for (var i = 0, len = this.coverages.length; i < len; i++) {
+ var coverage = this.coverages[i];
+ if (coverage.enabled && coverage.maxElevation > maxElevation) {
+ maxElevation = coverage.maxElevation;
+ }
+ }
+
+ return maxElevation !== -Number.MAX_VALUE ? maxElevation : 0; // no coverages or all coverages disabled
+ }
+ }
+});
+
+/**
+ * Internal use only
+ * Used to assign unique IDs to elevation models for use in their state key.
+ * @type {Number}
+ * @ignore
+ */
+ElevationModel.idPool = 0;
+
+/**
+ * Internal use only
+ * Sets the state key to a new unique value.
+ * @ignore
+ */
+ElevationModel.prototype.computeStateKey = function () {
+ this.id = ++ElevationModel.idPool;
+ this.stateKey = "elevationModel " + this.id.toString() + " ";
+};
+
+/**
+ * Internal use only
+ * The comparison function used for sorting elevation coverages.
+ * @ignore
+ */
+ElevationModel.prototype.coverageComparator = function (coverage1, coverage2) {
+ var res1 = coverage1.resolution;
+ var res2 = coverage2.resolution;
+ // sort from lowest resolution to highest
+ return res1 > res2 ? -1 : res1 === res2 ? 0 : 1;
+};
+
+/**
+ * Internal use only
+ * Perform common actions required when the list of available coverages changes.
+ * @ignore
+ */
+ElevationModel.prototype.performCoverageListChangedActions = function () {
+ if (this.coverages.length > 1) {
+ this.coverages.sort(this.coverageComparator);
+ }
+
+ this.computeStateKey();
+};
+
+/**
+ * Adds an elevation coverage to this elevation model and sorts the list. Duplicate coverages will be ignored.
+ *
+ * @param coverage The elevation coverage to add.
+ * @return {Boolean} true if the ElevationCoverage as added; false if the coverage was a duplicate.
+ * @throws ArgumentError if the specified elevation coverage is null.
+ */
+ElevationModel.prototype.addCoverage = function (coverage) {
+ if (!coverage) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "addCoverage", "missingCoverage"));
+ }
+
+ if (!this.containsCoverage(coverage)) {
+ this.coverages.push(coverage);
+ this.performCoverageListChangedActions();
+ return true;
+ }
+
+ return false;
+};
+
+/**
+ * Removes all elevation coverages from this elevation model.
+ */
+ElevationModel.prototype.removeAllCoverages = function () {
+ if (this.coverages.length > 0) {
+ this.coverages = [];
+ this.performCoverageListChangedActions();
+ }
+};
+
+/**
+ * Removes a specific elevation coverage from this elevation model.
+ *
+ * @param coverage The elevation model to remove.
+ *
+ * @throws ArgumentError if the specified elevation coverage is null.
+ */
+ElevationModel.prototype.removeCoverage = function (coverage) {
+ if (!coverage) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "removeCoverage", "missingCoverage"));
+ }
+
+ var index = this.coverages.indexOf(coverage);
+ if (index >= 0) {
+ this.coverages.splice(index, 1);
+ this.performCoverageListChangedActions();
+ }
+};
+
+/**
+ * Returns true if this ElevationModel contains the specified ElevationCoverage, and false otherwise.
+ *
+ * @param coverage the ElevationCoverage to test.
+ * @return {Boolean} true if the ElevationCoverage is in this ElevationModel; false otherwise.
+ * @throws ArgumentError if the ElevationCoverage is null.
+ */
+ElevationModel.prototype.containsCoverage = function (coverage) {
+ if (!coverage) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "containsCoverage", "missingCoverage"));
+ }
+ var index = this.coverages.indexOf(coverage);
+ return index >= 0;
+};
+
+/**
+ * Returns the minimum and maximum elevations within a specified sector.
+ * @param {Sector} sector The sector for which to determine extreme elevations.
+ * @returns {Number[]} An array containing the minimum and maximum elevations within the specified sector. If no coverage
+ * can satisfy the request, a min and max of zero is returned.
+ * @throws {ArgumentError} If the specified sector is null or undefined.
+ */
+ElevationModel.prototype.minAndMaxElevationsForSector = function (sector) {
+ if (!sector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "minAndMaxElevationsForSector", "missingSector"));
+ }
+
+ // Initialize the min and max elevations to the largest and smallest numbers, respectively. This has the
+ // effect of moving the extremes with each subsequent coverage as needed, without unintentionally capturing
+ // zero elevation. If we initialized this array with zeros the result would always contain zero, even when
+ // elevations in the sector are all above or below zero. This is critical for tile bounding boxes.
+
+ var result = [Number.MAX_VALUE, -Number.MAX_VALUE];
+
+ for (var i = this.coverages.length - 1; i >= 0; i--) {
+ var coverage = this.coverages[i];
+ if (coverage.enabled && coverage.coverageSector.intersects(sector)) {
+ if (coverage.minAndMaxElevationsForSector(sector, result)) {
+ break; // coverage completely fills the sector, ignore the remaining coverages
+ }
+ }
+ }
+
+ return result[0] !== Number.MAX_VALUE ? result : [0, 0]; // no coverages, all coverages disabled, or no coverages intersect the sector
+};
+
+/**
+ * Returns the elevation at a specified location.
+ * @param {Number} latitude The location's latitude in degrees.
+ * @param {Number} longitude The location's longitude in degrees.
+ * @returns {Number} The elevation at the specified location, in meters. Returns zero if the location is
+ * outside the coverage area of this model.
+ */
+ElevationModel.prototype.elevationAtLocation = function (latitude, longitude) {
+ var i, n = this.coverages.length;
+ for (i = n - 1; i >= 0; i--) {
+ var coverage = this.coverages[i];
+ if (coverage.enabled && coverage.coverageSector.containsLocation(latitude, longitude)) {
+ var elevation = coverage.elevationAtLocation(latitude, longitude);
+ if (elevation !== null) {
+ return elevation;
+ }
+ }
+ }
+
+ return 0;
+};
+
+/**
+ * Internal use only
+ * Returns the index of the coverage most closely matching the supplied resolution and overlapping the supplied
+ * sector or point area of interest. At least one area of interest parameter must be non-null.
+ * @param {Sector} sector An optional sector area of interest. Setting this parameter to null will cause it to be ignored.
+ * @param {Location} location An optional point area of interest. Setting this parameter to null will cause it to be ignored.
+ * @param {Number} targetResolution The desired elevation resolution, in degrees. (To compute degrees from
+ * meters, divide the number of meters by the globe's radius to obtain radians and convert the result to degrees.)
+ * @returns {Number} The index of the coverage most closely matching the requested resolution.
+ * @ignore
+ */
+ElevationModel.prototype.preferredCoverageIndex = function (sector, location, targetResolution) {
+
+ var i,
+ n = this.coverages.length,
+ minResDiff = Number.MAX_VALUE,
+ minDiffIdx = -1;
+
+ for (i = 0; i < n; i++) {
+ var coverage = this.coverages[i],
+ validCoverage = coverage.enabled && (sector !== null && coverage.coverageSector.intersects(sector) ||
+ location !== null && coverage.coverageSector.containsLocation(location.latitude, location.longitude));
+ if (validCoverage) {
+ var resDiff = Math.abs(coverage.resolution - targetResolution);
+ if (resDiff > minResDiff) {
+ return minDiffIdx;
+ }
+ minResDiff = resDiff;
+ minDiffIdx = i;
+ }
+ }
+
+ return minDiffIdx;
+};
+
+/**
+ * Returns the best coverage available for a particular resolution,
+ * @param {Number} latitude The location's latitude in degrees.
+ * @param {Number} longitude The location's longitude in degrees.
+ * @param {Number} targetResolution The desired elevation resolution, in degrees. (To compute degrees from
+ * meters, divide the number of meters by the globe's radius to obtain radians and convert the result to degrees.)
+ * @returns {ElevationCoverage} The coverage most closely matching the requested resolution. Returns null if no coverage is available at this
+ * location.
+ * @throws {ArgumentError} If the specified resolution is not positive.
+ */
+ElevationModel.prototype.bestCoverageAtLocation = function (latitude, longitude, targetResolution) {
+
+ if (!targetResolution || targetResolution < 0) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "bestCoverageAtLocation", "invalidResolution"));
+ }
+
+ this.scratchLocation.set(latitude, longitude);
+ var preferredIndex = this.preferredCoverageIndex(null, this.scratchLocation, targetResolution);
+ if (preferredIndex >= 0) {
+ return this.coverages[preferredIndex];
+ }
+
+ return null;
+};
+
+/**
+ * Returns the elevations at locations within a specified sector.
+ * @param {Sector} sector The sector for which to determine the elevations.
+ * @param {Number} numLat The number of latitudinal sample locations within the sector.
+ * @param {Number} numLon The number of longitudinal sample locations within the sector.
+ * @param {Number} targetResolution The desired elevation resolution, in degrees. (To compute degrees from
+ * meters, divide the number of meters by the globe's radius to obtain radians and convert the result to degrees.)
+ * @param {Number[]} result An array in which to return the requested elevations.
+ * @returns {Number} The resolution actually achieved, which may be greater than that requested if the
+ * elevation data for the requested resolution is not currently available.
+ * @throws {ArgumentError} If the specified sector, targetResolution, or result array is null or undefined, or if either of the
+ * specified numLat or numLon values is less than one.
+ */
+ElevationModel.prototype.elevationsForGrid = function (sector, numLat, numLon, targetResolution, result) {
+ if (!sector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "elevationsForGrid", "missingSector"));
+ }
+
+ if (!numLat || !numLon || numLat < 1 || numLon < 1) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "elevationsForGrid",
+ "The specified number of latitudinal or longitudinal positions is less than one."));
+ }
+
+ if (!targetResolution) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "elevationsForGrid", "missingTargetResolution"));
+ }
+
+ if (!result) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "ElevationModel", "elevationsForGrid", "missingResult"));
+ }
+
+ result.fill(NaN);
+ var resolution = Number.MAX_VALUE,
+ resultFilled = false,
+ preferredIndex = this.preferredCoverageIndex(sector, null, targetResolution);
+
+ if (preferredIndex >= 0) {
+ for (var i = preferredIndex; !resultFilled && i >= 0; i--) {
+ var coverage = this.coverages[i];
+ if (coverage.enabled && coverage.coverageSector.intersects(sector)) {
+ resultFilled = coverage.elevationsForGrid(sector, numLat, numLon, result);
+ if (resultFilled) {
+ resolution = coverage.resolution;
+ }
+ }
+ }
+ }
+
+ if (!resultFilled) {
+ var n = result.length;
+ for (i = 0; i < n; i++) {
+ if (isNaN(result[i])) {
+ result[i] = 0;
+ }
+ }
+ }
+
+ return resolution;
+};
+
+export default ElevationModel;
+
diff --git a/web/test/WebWorldWind/src/globe/GebcoElevationCoverage.js b/web/test/WebWorldWind/src/globe/GebcoElevationCoverage.js
new file mode 100644
index 00000000..211d36c8
--- /dev/null
+++ b/web/test/WebWorldWind/src/globe/GebcoElevationCoverage.js
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports GebcoElevationCoverage
+ */
+import Sector from '../geom/Sector';
+import TiledElevationCoverage from '../globe/TiledElevationCoverage';
+import WmsUrlBuilder from '../util/WmsUrlBuilder';
+
+
+/**
+ * Constructs an Earth elevation coverage using GEBCO data.
+ * @alias GebcoElevationCoverage
+ * @constructor
+ * @augments TiledElevationCoverage
+ * @classdesc Provides elevations for Earth. Elevations are drawn from the NASA WorldWind elevation service.
+ */
+function GebcoElevationCoverage() {
+ TiledElevationCoverage.call(this, {
+ coverageSector: Sector.FULL_SPHERE,
+ resolution: 0.008333333333333,
+ retrievalImageFormat: "application/bil16",
+ minElevation: -11000,
+ maxElevation: 8850,
+ urlBuilder: new WmsUrlBuilder("https://worldwind26.arc.nasa.gov/elev", "GEBCO", "", "1.3.0")
+ });
+
+ this.displayName = "GEBCO Earth Elevation Coverage";
+}
+
+GebcoElevationCoverage.prototype = Object.create(TiledElevationCoverage.prototype);
+
+export default GebcoElevationCoverage;
diff --git a/web/test/WebWorldWind/src/globe/Globe.js b/web/test/WebWorldWind/src/globe/Globe.js
new file mode 100644
index 00000000..1e30482a
--- /dev/null
+++ b/web/test/WebWorldWind/src/globe/Globe.js
@@ -0,0 +1,651 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports Globe
+ */
+import Angle from '../geom/Angle';
+import ArgumentError from '../error/ArgumentError';
+import BoundingBox from '../geom/BoundingBox';
+import Logger from '../util/Logger';
+import ProjectionWgs84 from '../projections/ProjectionWgs84';
+import Sector from '../geom/Sector';
+import Tessellator from '../globe/Tessellator';
+import Vec3 from '../geom/Vec3';
+
+
+/**
+ * Constructs an ellipsoidal Globe with default radii for Earth (WGS84).
+ * @alias Globe
+ * @constructor
+ * @classdesc Represents an ellipsoidal globe. The default configuration represents Earth but may be changed.
+ * To configure for another planet, set the globe's equatorial and polar radii properties and its
+ * eccentricity-squared property.
+ *
+ * A globe uses a Cartesian coordinate system whose origin is at the globe's center. It's Y axis points to the + * north pole, the Z axis points to the intersection of the prime meridian and the equator, + * and the X axis completes a right-handed coordinate system, is in the equatorial plane and 90 degrees east + * of the Z axis. + *
+ * All Cartesian coordinates and elevations are in meters. + + * @param {ElevationModel} elevationModel The elevation model to use for this globe. + * @param {GeographicProjection} projection The projection to apply to the globe. May be null or undefined, + * in which case no projection is applied and the globe is a WGS84 ellipsoid. + * @throws {ArgumentError} If the specified elevation model is null or undefined. + */ +function Globe(elevationModel, projection) { + if (!elevationModel) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", + "constructor", "Elevation model is null or undefined.")); + } + /** + * This globe's elevation model. + * @type {ElevationModel} + */ + this.elevationModel = elevationModel; + + /** + * This globe's equatorial radius in meters. + * + * @type {Number} + * @default WGS 84 semi-major axis (6378137.0 meters) + */ + this.equatorialRadius = WorldWind.WGS84_SEMI_MAJOR_AXIS; + var f = 1 / WorldWind.WGS84_INVERSE_FLATTENING; + + /** + * This globe's polar radius in meters. + * @type {Number} + * @default WGS 84 semi-minor axis (6356752.3142 meters). Taken from NGA.STND.0036_1.0.0_WGS84, section 3.2. + */ + this.polarRadius = this.equatorialRadius * (1 - f); + + /** + * This globe's eccentricity squared. + * @type {Number} + * @default WGS 84 first eccentricity squared (6.694379990141e-3). Taken from NGA.STND.0036_1.0.0_WGS84, section 3.3. + */ + this.eccentricitySquared = 2 * f - f * f; + + /** + * The tessellator used to create this globe's terrain. + * @type {Tessellator} + */ + this.tessellator = new Tessellator(); + + // Internal. Intentionally not documented. + this._projection = projection || new ProjectionWgs84(); + + // Internal. Intentionally not documented. + this._offset = 0; + + // Internal. Intentionally not documented. + this.offsetVector = new Vec3(0, 0, 0); + + // A unique ID for this globe. Intentionally not documented. + this.id = ++Globe.idPool; + + this._stateKey = "globe " + this.id.toString() + " "; +} + +Globe.idPool = 0; // Used to assign unique IDs to globes for use in their state keys. + +Object.defineProperties(Globe.prototype, { + /** + * A string identifying this globe's current state. Used to compare states during rendering to + * determine whether globe-state dependent cached values must be updated. Applications typically do not + * interact with this property. + * @memberof Globe.prototype + * @readonly + * @type {String} + */ + stateKey: { + get: function () { + return this._stateKey + this.elevationModel.stateKey + "offset " + this.offset.toString() + " " + + this.projection.stateKey; + } + }, + + /** + * Indicates whether this globe is 2D and continuous with itself -- that it should scroll continuously + * horizontally. + * @memberof Globe.prototype + * @readonly + * @type {Boolean} + */ + continuous: { + get: function () { + return this.projection.continuous; + } + }, + + /** + * The projection used by this globe. + * @memberof Globe.prototype + * @default {@link ProjectionWgs84} + * @type {GeographicProjection} + */ + projection: { + get: function () { + return this._projection; + }, + set: function (projection) { + if (!projection) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", + "projection", "missingProjection")); + } + + if (this.projection != projection) { + this.tessellator = new Tessellator(); + } + this._projection = projection; + } + }, + + /** + * The projection limits of the associated projection. + * @memberof Globe.prototype + * @type {Sector} + */ + projectionLimits: { + get: function () { + return this._projection.projectionLimits; + } + }, + + /** + * An offset to apply to this globe when translating between Geographic positions and Cartesian points. + * Used during scrolling to position points appropriately. + * Applications typically do not access this property. It is used by the associated globe. + * @memberof Globe.prototype + * @type {Number} + */ + offset: { + get: function () { + return this._offset; + }, + set: function (offset) { + this._offset = offset; + this.offsetVector[0] = offset * 2 * Math.PI * this.equatorialRadius; + } + } +}); + +/** + * Indicates whether this is a 2D globe. + * @returns {Boolean} true if this is a 2D globe, otherwise false. + */ +Globe.prototype.is2D = function () { + return this.projection.is2D; +}; + +/** + * Computes a Cartesian point from a specified position. + * See this class' Overview section for a description of the Cartesian coordinate system used. + * @param {Number} latitude The position's latitude. + * @param {Number} longitude The position's longitude. + * @param {Number} altitude The position's altitude. + * @param {Vec3} result A reference to a pre-allocated {@link Vec3} in which to return the computed X, + * Y and Z Cartesian coordinates. + * @returns {Vec3} The result argument. + * @throws {ArgumentError} If the specified result argument is null or undefined. + */ +Globe.prototype.computePointFromPosition = function (latitude, longitude, altitude, result) { + if (!result) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "computePointFromPosition", + "missingResult")); + } + + return this.projection.geographicToCartesian(this, latitude, longitude, altitude, this.offsetVector, result); +}; + +/** + * Computes a Cartesian point from a specified location. + * See this class' Overview section for a description of the Cartesian coordinate system used. + * @param {Number} latitude The position's latitude. + * @param {Number} longitude The position's longitude. + * @param {Vec3} result A reference to a pre-allocated {@link Vec3} in which to return the computed X, + * Y and Z Cartesian coordinates. + * @returns {Vec3} The result argument. + * @throws {ArgumentError} If the specified result argument is null or undefined. + */ +Globe.prototype.computePointFromLocation = function (latitude, longitude, result) { + if (!result) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "computePointFromLocation", + "missingResult")); + } + + return this.computePointFromPosition(latitude, longitude, 0, result); +}; + +/** + * Computes a grid of Cartesian points within a specified sector and relative to a specified Cartesian + * reference point. + *
+ * This method is used to compute a collection of points within a sector. It is used by tessellators to + * efficiently generate a tile's interior points. The number of points to generate is indicated by the numLon + * and numLat parameters. + *
+ * For each implied position within the sector, an elevation value is specified via an array of elevations. The
+ * calculation at each position incorporates the associated elevation. There must be numLat x numLon elevations
+ * in the array.
+ *
+ * @param {Sector} sector The sector for which to compute the points.
+ * @param {Number} numLat The number of latitudinal points in the grid.
+ * @param {Number} numLon The number of longitudinal points in the grid.
+ * @param {Number[]} elevations An array of elevations to incorporate in the point calculations. There must be
+ * one elevation value in the array for each generated point. Elevations are in meters. There must be
+ * numLat x numLon elevations in the array.
+ * @param {Vec3} referencePoint The X, Y and Z Cartesian coordinates to subtract from the computed coordinates.
+ * This makes the computed coordinates relative to the specified point.
+ * @param {Float32Array} result A typed array to hold the computed coordinates. It must be at least of
+ * size numLat x numLon. The points are returned in row major order, beginning with the row of minimum latitude.
+ * @returns {Float32Array} The specified result argument.
+ * @throws {ArgumentError} if the specified sector, elevations array or results arrays are null or undefined, or
+ * if the lengths of any of the arrays are insufficient.
+ */
+Globe.prototype.computePointsForGrid = function (sector, numLat, numLon, elevations, referencePoint, result) {
+ if (!sector) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe",
+ "computePointsFromPositions", "missingSector"));
+ }
+
+ if (numLat < 1 || numLon < 1) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "computePointsFromPositions",
+ "Number of latitude or longitude locations is less than one."));
+ }
+
+ var numPoints = numLat * numLon;
+ if (!elevations || elevations.length < numPoints) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "computePointsFromPositions",
+ "Elevations array is null, undefined or insufficient length."));
+ }
+
+ if (!result || result.length < numPoints) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "computePointsFromPositions",
+ "Result array is null, undefined or insufficient length."));
+ }
+
+ return this.projection.geographicToCartesianGrid(this, sector, numLat, numLon, elevations, referencePoint,
+ this.offsetVector, result);
+};
+
+/**
+ * Computes a geographic position from a specified Cartesian point.
+ *
+ * See this class' Overview section for a description of the Cartesian coordinate system used.
+ *
+ * @param {Number} x The X coordinate.
+ * @param {Number} y The Y coordinate.
+ * @param {Number} z The Z coordinate.
+ * @param {Position} result A pre-allocated {@link Position} instance in which to return the computed position.
+ * @returns {Position} The specified result position.
+ * @throws {ArgumentError} If the specified result argument is null or undefined.
+ */
+Globe.prototype.computePositionFromPoint = function (x, y, z, result) {
+ if (!result) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "computePositionFromPoint",
+ "missingResult"));
+ }
+
+ this.projection.cartesianToGeographic(this, x, y, z, this.offsetVector, result);
+
+ // Wrap if the globe is continuous.
+ if (this.continuous) {
+ if (result.longitude < -180) {
+ result.longitude += 360;
+ } else if (result.longitude > 180) {
+ result.longitude -= 360;
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Computes the radius of this globe at a specified location.
+ * @param {Number} latitude The locations' latitude.
+ * @param {Number} longitude The locations' longitude.
+ * @returns {Number} The radius at the specified location.
+ */
+Globe.prototype.radiusAt = function (latitude, longitude) {
+ var sinLat = Math.sin(latitude * Angle.DEGREES_TO_RADIANS),
+ rpm = this.equatorialRadius / Math.sqrt(1.0 - this.eccentricitySquared * sinLat * sinLat);
+
+ return rpm * Math.sqrt(1.0 + (this.eccentricitySquared * this.eccentricitySquared - 2.0 * this.eccentricitySquared) * sinLat * sinLat);
+};
+
+/**
+ * Computes the normal vector to this globe's surface at a specified location.
+ * @param {Number} latitude The location's latitude.
+ * @param {Number} longitude The location's longitude.
+ * @param {Vec3} result A pre-allocated {@Link Vec3} instance in which to return the computed vector. The returned
+ * normal vector is unit length.
+ * @returns {Vec3} The specified result vector. The returned normal vector is unit length.
+ * @throws {ArgumentError} If the specified result argument is null or undefined.
+ */
+Globe.prototype.surfaceNormalAtLocation = function (latitude, longitude, result) {
+ if (!result) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "surfaceNormalAtLocation",
+ "missingResult"));
+ }
+
+ // For backwards compatibility, check whether the projection defines a surfaceNormalAtLocation function
+ // before calling it. If it's not available, use the old code to compute the normal.
+ if (this.projection.surfaceNormalAtLocation) {
+ return this.projection.surfaceNormalAtLocation(this, latitude, longitude, result);
+ }
+
+ if (this.is2D()) {
+ result[0] = 0;
+ result[1] = 0;
+ result[2] = 1;
+
+ return result;
+ }
+
+ var cosLat = Math.cos(latitude * Angle.DEGREES_TO_RADIANS),
+ cosLon = Math.cos(longitude * Angle.DEGREES_TO_RADIANS),
+ sinLat = Math.sin(latitude * Angle.DEGREES_TO_RADIANS),
+ sinLon = Math.sin(longitude * Angle.DEGREES_TO_RADIANS);
+
+ result[0] = cosLat * sinLon;
+ result[1] = sinLat;
+ result[2] = cosLat * cosLon;
+
+ return result.normalize();
+};
+
+/**
+ * Computes the normal vector to this globe's surface at a specified Cartesian point.
+ * @param {Number} x The point's X coordinate.
+ * @param {Number} y The point's Y coordinate.
+ * @param {Number} z The point's Z coordinate.
+ * @param {Vec3} result A pre-allocated {@Link Vec3} instance in which to return the computed vector. The returned
+ * normal vector is unit length.
+ * @returns {Vec3} The specified result vector.
+ * @throws {ArgumentError} If the specified result argument is null or undefined.
+ */
+Globe.prototype.surfaceNormalAtPoint = function (x, y, z, result) {
+ if (!result) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "surfaceNormalAtPoint",
+ "missingResult"));
+ }
+
+ // For backwards compatibility, check whether the projection defines a surfaceNormalAtPoint function
+ // before calling it. If it's not available, use the old code to compute the normal.
+ if (this.projection.surfaceNormalAtPoint) {
+ return this.projection.surfaceNormalAtPoint(this, x, y, z, result);
+ }
+
+ if (this.is2D()) {
+ result[0] = 0;
+ result[1] = 0;
+ result[2] = 1;
+
+ return result;
+ }
+
+ var eSquared = this.equatorialRadius * this.equatorialRadius,
+ polSquared = this.polarRadius * this.polarRadius;
+
+ result[0] = x / eSquared;
+ result[1] = y / polSquared;
+ result[2] = z / eSquared;
+
+ return result.normalize();
+};
+
+/**
+ * Computes the north-pointing tangent vector to this globe's surface at a specified location.
+ * @param {Number} latitude The location's latitude.
+ * @param {Number} longitude The location's longitude.
+ * @param {Vec3} result A pre-allocated {@Link Vec3} instance in which to return the computed vector. The returned
+ * tangent vector is unit length.
+ * @returns {Vec3} The specified result vector.
+ * @throws {ArgumentError} If the specified result argument is null or undefined.
+ */
+Globe.prototype.northTangentAtLocation = function (latitude, longitude, result) {
+ if (!result) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "northTangentAtLocation",
+ "missingResult"));
+ }
+
+ return this.projection.northTangentAtLocation(this, latitude, longitude, result);
+};
+
+/**
+ * Computes the north-pointing tangent vector to this globe's surface at a specified Cartesian point.
+ * @param {Number} x The point's X coordinate.
+ * @param {Number} y The point's Y coordinate.
+ * @param {Number} z The point's Z coordinate.
+ * @param {Vec3} result A pre-allocated {@Link Vec3} instance in which to return the computed vector. The returned
+ * tangent vector is unit length.
+ * @returns {Vec3} The specified result vector.
+ * @throws {ArgumentError} If the specified result argument is null or undefined.
+ */
+Globe.prototype.northTangentAtPoint = function (x, y, z, result) {
+ if (!result) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "northTangentAtPoint",
+ "missingResult"));
+ }
+
+ return this.projection.northTangentAtPoint(this, x, y, z, this.offsetVector, result);
+};
+
+/**
+ * Indicates whether this globe intersects a specified frustum.
+ * @param {Frustum} frustum The frustum to test.
+ * @returns {Boolean} true if this globe intersects the frustum, otherwise false.
+ * @throws {ArgumentError} If the specified frustum is null or undefined.
+ */
+Globe.prototype.intersectsFrustum = function (frustum) {
+ if (!frustum) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "intersectsFrustum", "missingFrustum"));
+ }
+
+ if (this.is2D()) {
+ var bbox = new BoundingBox();
+ bbox.setToSector(Sector.FULL_SPHERE, this, this.elevationModel.minElevation,
+ this.elevationModel.maxElevation);
+
+ return bbox.intersectsFrustum(frustum);
+ }
+
+ if (frustum.far.distance <= this.equatorialRadius)
+ return false;
+ if (frustum.left.distance <= this.equatorialRadius)
+ return false;
+ if (frustum.right.distance <= this.equatorialRadius)
+ return false;
+ if (frustum.top.distance <= this.equatorialRadius)
+ return false;
+ if (frustum.bottom.distance <= this.equatorialRadius)
+ return false;
+ if (frustum.near.distance <= this.equatorialRadius)
+ return false;
+
+ return true;
+};
+
+/**
+ * Computes the first intersection of this globe with a specified line. The line is interpreted as a ray;
+ * intersection points behind the line's origin are ignored.
+ * @param {Line} line The line to intersect with this globe.
+ * @param {Vec3} result A pre-allocated Vec3 in which to return the computed point.
+ * @returns {boolean} true If the ray intersects the globe, otherwise false.
+ * @throws {ArgumentError} If the specified line or result argument is null or undefined.
+ */
+Globe.prototype.intersectsLine = function (line, result) {
+ if (!line) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "intersectWithRay", "missingLine"));
+ }
+
+ if (!result) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "intersectsLine", "missingResult"));
+ }
+
+ // Taken from "Mathematics for 3D Game Programming and Computer Graphics, Third Edition", Section 6.2.3.
+ //
+ // Note that the parameter n from equations 6.70 and 6.71 is omitted here. For an ellipsoidal globe this
+ // parameter is always 1, so its square and its product with any other value simplifies to the identity.
+
+ var vx = line.direction[0],
+ vy = line.direction[1],
+ vz = line.direction[2],
+ sx = line.origin[0],
+ sy = line.origin[1],
+ sz = line.origin[2],
+ t;
+
+ if (this.is2D()) {
+ if (vz == 0 && sz != 0) { // ray is parallel to and not coincident with the XY plane
+ return false;
+ }
+
+ t = -sz / vz; // intersection distance, simplified for the XY plane
+ if (t < 0) { // intersection is behind the ray's origin
+ return false;
+ }
+
+ result[0] = sx + vx * t;
+ result[1] = sy + vy * t;
+ result[2] = sz + vz * t;
+
+ return true;
+ } else {
+ var eqr = this.equatorialRadius, eqr2 = eqr * eqr, m = eqr / this.polarRadius, m2 = m * m, a, b, c, d;
+
+ a = vx * vx + m2 * vy * vy + vz * vz;
+ b = 2 * (sx * vx + m2 * sy * vy + sz * vz);
+ c = sx * sx + m2 * sy * sy + sz * sz - eqr2;
+ d = b * b - 4 * a * c; // discriminant
+
+ if (d < 0) {
+ return false;
+ }
+
+ t = (-b - Math.sqrt(d)) / (2 * a);
+ // check if the nearest intersection point is in front of the origin of the ray
+ if (t > 0) {
+ result[0] = sx + vx * t;
+ result[1] = sy + vy * t;
+ result[2] = sz + vz * t;
+ return true;
+ }
+
+ t = (-b + Math.sqrt(d)) / (2 * a);
+ // check if the second intersection point is in the front of the origin of the ray
+ if (t > 0) {
+ result[0] = sx + vx * t;
+ result[1] = sy + vy * t;
+ result[2] = sz + vz * t;
+ return true;
+ }
+
+ // the intersection points were behind the origin of the provided line
+ return false;
+ }
+};
+
+/**
+ * Returns the time at which any elevations associated with this globe last changed.
+ * @returns {Number} The time in milliseconds relative to the Epoch of the most recent elevation change.
+ */
+Globe.prototype.elevationTimestamp = function () {
+ return this.elevationModel.timestamp;
+};
+
+/**
+ * Returns this globe's minimum elevation.
+ * @returns {Number} This globe's minimum elevation.
+ */
+Globe.prototype.minElevation = function () {
+ return this.elevationModel.minElevation;
+};
+
+/**
+ * Returns this globe's maximum elevation.
+ * @returns {Number} This globe's maximum elevation.
+ */
+Globe.prototype.maxElevation = function () {
+ return this.elevationModel.maxElevation;
+};
+
+/**
+ * Returns the minimum and maximum elevations within a specified sector of this globe.
+ * @param {Sector} sector The sector for which to determine extreme elevations.
+ * @returns {Number[]} The An array containing the minimum and maximum elevations.
+ * @throws {ArgumentError} If the specified sector is null or undefined.
+ */
+Globe.prototype.minAndMaxElevationsForSector = function (sector) {
+ if (!sector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "minAndMaxElevationsForSector",
+ "missingSector"));
+ }
+
+ return this.elevationModel.minAndMaxElevationsForSector(sector);
+};
+
+/**
+ * Returns the elevation at a specified location.
+ * @param {Number} latitude The location's latitude in degrees.
+ * @param {Number} longitude The location's longitude in degrees.
+ * @returns {Number} The elevation at the specified location, in meters. Returns zero if the location is
+ * outside the coverage area of this elevation model.
+ */
+Globe.prototype.elevationAtLocation = function (latitude, longitude) {
+ return this.elevationModel.elevationAtLocation(latitude, longitude);
+};
+
+/**
+ * Returns the elevations at locations within a specified sector.
+ * @param {Sector} sector The sector for which to determine the elevations.
+ * @param {Number} numLat The number of latitudinal sample locations within the sector.
+ * @param {Number} numLon The number of longitudinal sample locations within the sector.
+ * @param {Number} targetResolution The desired elevation resolution, in degrees. (To compute degrees from
+ * meters, divide the number of meters by the globe's radius to obtain radians and convert the result to degrees.)
+ * @param {Number[]} result An array in which to return the requested elevations.
+ * @returns {Number} The resolution actually achieved, which may be greater than that requested if the
+ * elevation data for the requested resolution is not currently available.
+ * @throws {ArgumentError} If the specified sector or result array is null or undefined, or if either of the
+ * specified numLat or numLon values is less than one.
+ */
+Globe.prototype.elevationsForGrid = function (sector, numLat, numLon, targetResolution, result) {
+ if (!sector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Globe", "elevationsForSector", "missingSector"));
+ }
+
+ if (numLat <= 0 || numLon <= 0) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe",
+ "elevationsForSector", "numLat or numLon is less than 1"));
+ }
+
+ if (!result || result.length < numLat * numLon) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Globe",
+ "elevationsForSector", "missingArray"));
+ }
+
+ return this.elevationModel.elevationsForGrid(sector, numLat, numLon, targetResolution, result);
+};
+
+export default Globe;
+
diff --git a/web/test/WebWorldWind/src/globe/Terrain.js b/web/test/WebWorldWind/src/globe/Terrain.js
new file mode 100644
index 00000000..f3053ad4
--- /dev/null
+++ b/web/test/WebWorldWind/src/globe/Terrain.js
@@ -0,0 +1,226 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports Terrain
+ */
+import ArgumentError from '../error/ArgumentError';
+import Logger from '../util/Logger';
+import Vec3 from '../geom/Vec3';
+
+
+/**
+ * Constructs a Terrain object.
+ * @alias Terrain
+ * @constructor
+ * @classdesc Represents terrain and provides functions for computing points on or relative to the terrain.
+ * Applications do not typically interact directly with this class.
+ */
+function Terrain(globe, tessellator, terrainTiles, verticalExaggeration) {
+
+ /**
+ * The globe associated with this terrain.
+ * @type {Globe}
+ */
+ this.globe = globe;
+
+ /**
+ * The vertical exaggeration of this terrain.
+ * @type {Number}
+ */
+ this.verticalExaggeration = verticalExaggeration;
+
+ /**
+ * The sector spanned by this terrain.
+ * @type {Sector}
+ */
+ this.sector = terrainTiles.sector;
+
+ /**
+ * The tessellator used to generate this terrain.
+ * @type {Tessellator}
+ */
+ this.tessellator = tessellator;
+
+ /**
+ * The surface geometry for this terrain
+ * @type {TerrainTile[]}
+ */
+ this.surfaceGeometry = terrainTiles.tileArray;
+
+ /**
+ * A string identifying this terrain's current state. Used to compare states during rendering to
+ * determine whether state dependent cached values must be updated. Applications typically do not
+ * interact with this property.
+ * @readonly
+ * @type {String}
+ */
+ this.stateKey = globe.stateKey + " ve " + verticalExaggeration.toString();
+}
+
+Terrain.scratchPoint = new Vec3(0, 0, 0);
+
+/**
+ * Computes a Cartesian point at a location on the surface of this terrain.
+ * @param {Number} latitude The location's latitude.
+ * @param {Number} longitude The location's longitude.
+ * @param {Number} offset Distance above the terrain, in meters, at which to compute the point.
+ * @param {Vec3} result A pre-allocated Vec3 in which to return the computed point.
+ * @returns {Vec3} The specified result parameter, set to the coordinates of the computed point. If the
+ * specfied location is not within this terrain, the associated globe is used to compute the point.
+ * @throws {ArgumentError} If the specified result argument is null or undefined.
+ */
+Terrain.prototype.surfacePoint = function (latitude, longitude, offset, result) {
+ if (!result) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Terrain", "surfacePoint", "missingResult"));
+ }
+
+ for (var i = 0, len = this.surfaceGeometry.length; i < len; i++) {
+ if (this.surfaceGeometry[i].sector.containsLocation(latitude, longitude)) {
+ this.surfaceGeometry[i].surfacePoint(latitude, longitude, result);
+
+ if (offset) {
+ var normal = this.globe.surfaceNormalAtPoint(result[0], result[1], result[2], Terrain.scratchPoint);
+ result[0] += normal[0] * offset;
+ result[1] += normal[1] * offset;
+ result[2] += normal[2] * offset;
+ }
+
+ return result;
+ }
+ }
+
+ // No tile was found that contains the location, so approximate one using the globe.
+ var h = offset + this.globe.elevationAtLocation(latitude, longitude) * this.verticalExaggeration;
+ this.globe.computePointFromPosition(latitude, longitude, h, result);
+
+ return result;
+};
+
+/**
+ * Computes a Cartesian point at a location on the surface of this terrain according to a specified
+ * altitude mode.
+ * @param {Number} latitude The location's latitude.
+ * @param {Number} longitude The location's longitude.
+ * @param {Number} offset Distance above the terrain, in meters relative to the specified altitude mode, at
+ * which to compute the point.
+ * @param {String} altitudeMode The altitude mode to use to compute the point. Recognized values are
+ * WorldWind.ABSOLUTE, WorldWind.CLAMP_TO_GROUND and
+ * WorldWind.RELATIVE_TO_GROUND. The mode WorldWind.ABSOLUTE is used if the
+ * specified mode is null, undefined or unrecognized, or if the specified location is outside this terrain.
+ * @param {Vec3} result A pre-allocated Vec3 in which to return the computed point.
+ * @returns {Vec3} The specified result parameter, set to the coordinates of the computed point.
+ * @throws {ArgumentError} If the specified result argument is null or undefined.
+ */
+Terrain.prototype.surfacePointForMode = function (latitude, longitude, offset, altitudeMode, result) {
+ if (!result) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Terrain", "surfacePointForMode", "missingResult"));
+ }
+
+ if (!altitudeMode)
+ altitudeMode = WorldWind.ABSOLUTE;
+
+ if (altitudeMode === WorldWind.CLAMP_TO_GROUND) {
+ return this.surfacePoint(latitude, longitude, 0, result);
+ } else if (altitudeMode === WorldWind.RELATIVE_TO_GROUND) {
+ return this.surfacePoint(latitude, longitude, offset, result);
+ } else {
+ var height = offset * this.verticalExaggeration;
+ this.globe.computePointFromPosition(latitude, longitude, height, result);
+ return result;
+ }
+};
+
+/**
+ * Initializes rendering state to draw a succession of terrain tiles.
+ * @param {DrawContext} dc The current draw context.
+ */
+Terrain.prototype.beginRendering = function (dc) {
+ if (this.globe && this.globe.tessellator) {
+ this.globe.tessellator.beginRendering(dc);
+ }
+};
+
+/**
+ * Restores rendering state after drawing a succession of terrain tiles.
+ * @param {DrawContext} dc The current draw context.
+ */
+Terrain.prototype.endRendering = function (dc) {
+ if (this.globe && this.globe.tessellator) {
+ this.globe.tessellator.endRendering(dc);
+ }
+};
+
+/**
+ * Initializes rendering state for drawing a specified terrain tile.
+ * @param {DrawContext} dc The current draw context.
+ * @param {TerrainTile} terrainTile The terrain tile subsequently drawn via this tessellator's render function.
+ * @throws {ArgumentError} If the specified tile is null or undefined.
+ */
+Terrain.prototype.beginRenderingTile = function (dc, terrainTile) {
+ if (!terrainTile) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Terrain", "beginRenderingTile", "missingTile"));
+ }
+
+ if (this.globe && this.globe.tessellator) {
+ this.globe.tessellator.beginRenderingTile(dc, terrainTile);
+ }
+};
+
+/**
+ * Restores rendering state after drawing the most recent tile specified to
+ * [beginRenderingTile]{@link Terrain#beginRenderingTile}.
+ * @param {DrawContext} dc The current draw context.
+ * @param {TerrainTile} terrainTile The terrain tile most recently rendered.
+ * @throws {ArgumentError} If the specified tile is null or undefined.
+ */
+Terrain.prototype.endRenderingTile = function (dc, terrainTile) {
+ // Intentionally empty.
+};
+
+/**
+ * Renders a specified terrain tile.
+ * @param {DrawContext} dc The current draw context.
+ * @param {TerrainTile} terrainTile The terrain tile to render.
+ * @throws {ArgumentError} If the specified tile is null or undefined.
+ */
+Terrain.prototype.renderTile = function (dc, terrainTile) {
+ if (!terrainTile) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Terrain", "renderTile", "missingTile"));
+ }
+
+ if (this.globe && this.globe.tessellator) {
+ this.globe.tessellator.renderTile(dc, terrainTile);
+ }
+};
+
+/**
+ * Causes this terrain to perform the picking operations appropriate for the draw context's pick settings.
+ * Normally, this draws the terrain in a unique pick color and computes the picked terrain position. When the
+ * draw context is set to region picking mode this omits the computation of a picked terrain position.
+ * @param {DrawContext} dc The current draw context.
+ */
+Terrain.prototype.pick = function (dc) {
+ if (this.globe && this.globe.tessellator) {
+ this.globe.tessellator.pick(dc, this.surfaceGeometry, this); // use this terrain as the userObject
+ }
+};
+
+export default Terrain;
diff --git a/web/test/WebWorldWind/src/globe/TerrainTile.js b/web/test/WebWorldWind/src/globe/TerrainTile.js
new file mode 100644
index 00000000..b1a319fc
--- /dev/null
+++ b/web/test/WebWorldWind/src/globe/TerrainTile.js
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports TerrainTile
+ */
+import ArgumentError from '../error/ArgumentError';
+import Logger from '../util/Logger';
+import Matrix from '../geom/Matrix';
+import Tile from '../util/Tile';
+
+
+/**
+ * Constructs a terrain tile.
+ * @alias TerrainTile
+ * @constructor
+ * @augments Tile
+ * @classdesc Represents a portion of a globe's terrain. Applications typically do not interact directly with
+ * this class.
+ * @param {Sector} sector The sector this tile covers.
+ * @param {Level} level The level this tile is associated with.
+ * @param {Number} row This tile's row in the associated level.
+ * @param {Number} column This tile's column in the associated level.
+ * @throws {ArgumentError} If the specified sector or level is null or undefined or the row or column arguments
+ * are less than zero.
+ */
+function TerrainTile(sector, level, row, column) {
+ Tile.call(this, sector, level, row, column); // args are checked in the superclass' constructor
+
+ /**
+ * The transformation matrix that maps tile local coordinates to model coordinates.
+ * @type {Matrix}
+ */
+ this.transformationMatrix = Matrix.fromIdentity();
+
+ /**
+ * The tile's model coordinate points.
+ * @type {Float32Array}
+ */
+ this.points = null;
+
+ /**
+ * Indicates the state of this tile when the model coordinate points were last updated. This is used to
+ * invalidate the points when this tile's state changes.
+ * @type {String}
+ */
+ this.pointsStateKey = null;
+
+ /**
+ * Indicates the state of this tile when the model coordinate VBO was last uploaded to GL. This is used to
+ * invalidate the VBO when the tile's state changes.
+ * @type {String}
+ */
+ this.pointsVboStateKey = null;
+
+ // Internal use. Intentionally not documented.
+ this.neighborMap = {};
+ this.neighborMap[WorldWind.NORTH] = null;
+ this.neighborMap[WorldWind.SOUTH] = null;
+ this.neighborMap[WorldWind.EAST] = null;
+ this.neighborMap[WorldWind.WEST] = null;
+
+ // Internal use. Intentionally not documented.
+ this._stateKey = null;
+
+ // Internal use. Intentionally not documented.
+ this._elevationTimestamp = null;
+
+ // Internal use. Intentionally not documented.
+ this.scratchArray = [];
+}
+
+TerrainTile.prototype = Object.create(Tile.prototype);
+
+Object.defineProperties(TerrainTile.prototype, {
+ /**
+ * A string identifying the state of this tile as a function of the elevation model's timestamp and this
+ * tile's neighbors. Used to compare states during rendering to determine whether cached values must be
+ * updated. Applications typically do not interact with this property.
+ * @type {String}
+ * @memberof TerrainTile.prototype
+ * @readonly
+ */
+ stateKey: {
+ get: function () {
+ if (!this._stateKey) {
+ this._stateKey = this.computeStateKey();
+ }
+
+ return this._stateKey;
+ }
+ }
+});
+
+/**
+ * Indicates the level of the tile adjacent to this tile in a specified direction. This returns null when this
+ * tile has no neighbor in that direction.
+ * @param {String} direction The cardinal direction. Must be one of WorldWind.NORTH, WorldWind.SOUTH,
+ * WorldWind.EAST or WorldWind.WEST.
+ * @returns {Level} The neighbor tile's level in the specified direction, or null if there is no neighbor.
+ */
+TerrainTile.prototype.neighborLevel = function (direction) {
+ return this.neighborMap[direction];
+};
+
+/**
+ * Specifies the level of the tile adjacent to this tile in a specified direction.
+ * @param {String} direction The cardinal direction. Must be one of WorldWind.NORTH, WorldWind.SOUTH,
+ * WorldWind.EAST or WorldWind.WEST.
+ * @param {Level} level The neighbor tile's level in the specified direction, or null to indicate that there is
+ * no neighbor in that direction.
+ */
+TerrainTile.prototype.setNeighborLevel = function (direction, level) {
+ this.neighborMap[direction] = level;
+ this._stateKey = null; // cause updates to any neighbor-dependent cached state
+};
+
+/**
+ * Computes a point on the terrain at a specified location.
+ * @param {Number} latitude The location's latitude.
+ * @param {Number} longitude The location's longitude.
+ * @param {Vec3} result A pre-allocated Vec3 in which to return the computed point.
+ * @returns {Vec3} The result argument set to the computed point.
+ * @throws {ArgumentError} If the specified result argument is null or undefined.
+ */
+TerrainTile.prototype.surfacePoint = function (latitude, longitude, result) {
+ if (!result) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "TerrainTile", "surfacePoint", "missingResult"));
+ }
+
+ var tileSector = this.sector,
+ minLat = tileSector.minLatitude,
+ maxLat = tileSector.maxLatitude,
+ minLon = tileSector.minLongitude,
+ maxLon = tileSector.maxLongitude,
+ tileWidth = this.tileWidth,
+ tileHeight = this.tileHeight,
+ s, t, si, ti, rowStride, vertices, points, k, sf, tf, x, y, z;
+
+ // Compute the location's horizontal (s) and vertical (t) parameterized coordinates within the tiles 2D grid of
+ // points as a floating-point value in the range [0, tileWidth] and [0, tileHeight]. These coordinates indicate
+ // which cell contains the location, as well as the location's placement within the cell. Note that this method
+ // assumes that the caller has tested whether the location is contained within the tile's sector.
+ s = (longitude - minLon) / (maxLon - minLon) * tileWidth;
+ t = (latitude - minLat) / (maxLat - minLat) * tileHeight;
+
+ // Get the coordinates for the four vertices defining the cell this point is in. Tile vertices start in the lower
+ // left corner and proceed in row major order across the tile. The tile contains one more vertex per row or
+ // column than the tile width or height. Vertices in the points array are organized in the
+ // following order: lower-left, lower-right, upper-left, upper-right. The cell's diagonal starts at the
+ // lower-left vertex and ends at the upper-right vertex.
+ si = s < tileWidth ? Math.floor(s) : tileWidth - 1;
+ ti = t < tileHeight ? Math.floor(t) : tileHeight - 1;
+ rowStride = tileWidth + 1;
+
+ vertices = this.points;
+ points = this.scratchArray; // temporary working buffer
+ k = 3 * (si + ti * rowStride); // lower-left and lower-right vertices
+ for (var i = 0; i < 6; i++) {
+ points[i] = vertices[k + i];
+ }
+
+ k = 3 * (si + (ti + 1) * rowStride); // upper-left and upper-right vertices
+ for (var j = 6; j < 12; j++) {
+ points[j] = vertices[k + (j - 6)];
+ }
+
+ // Compute the location's corresponding point on the cell in tile local coordinates,
+ // given the fractional portion of the parameterized s and t coordinates. These values indicate the location's
+ // relative placement within the cell. The cell's vertices are defined in the following order: lower-left,
+ // lower-right, upper-left, upper-right. The cell's diagonal starts at the lower-right vertex and ends at the
+ // upper-left vertex.
+ sf = s < tileWidth ? s - Math.floor(s) : 1;
+ tf = t < tileHeight ? t - Math.floor(t) : 1;
+
+ if (sf > tf) {
+ result[0] = points[0] + sf * (points[3] - points[0]) + tf * (points[6] - points[0]);
+ result[1] = points[1] + sf * (points[4] - points[1]) + tf * (points[7] - points[1]);
+ result[2] = points[2] + sf * (points[5] - points[2]) + tf * (points[8] - points[2]);
+ }
+ else {
+ result[0] = points[9] + (1 - sf) * (points[6] - points[9]) + (1 - tf) * (points[3] - points[9]);
+ result[1] = points[10] + (1 - sf) * (points[7] - points[10]) + (1 - tf) * (points[4] - points[10]);
+ result[2] = points[11] + (1 - sf) * (points[8] - points[11]) + (1 - tf) * (points[5] - points[11]);
+ }
+
+ result[0] += this.referencePoint[0];
+ result[1] += this.referencePoint[1];
+ result[2] += this.referencePoint[2];
+
+ return result;
+};
+
+TerrainTile.prototype.update = function (dc) {
+ Tile.prototype.update.call(this, dc);
+
+ var elevationTimestamp = dc.globe.elevationTimestamp();
+ if (this._elevationTimestamp != elevationTimestamp) {
+ this._elevationTimestamp = elevationTimestamp;
+ this._stateKey = null; // cause updates to any elevation-dependent cached state
+ }
+};
+
+// Intentionally not documented.
+TerrainTile.prototype.computeStateKey = function () {
+ var array = [];
+ array.push(this._elevationTimestamp);
+ array.push(this.neighborMap[WorldWind.NORTH] ? this.neighborMap[WorldWind.NORTH].compare(this.level) : 0);
+ array.push(this.neighborMap[WorldWind.SOUTH] ? this.neighborMap[WorldWind.SOUTH].compare(this.level) : 0);
+ array.push(this.neighborMap[WorldWind.EAST] ? this.neighborMap[WorldWind.EAST].compare(this.level) : 0);
+ array.push(this.neighborMap[WorldWind.WEST] ? this.neighborMap[WorldWind.WEST].compare(this.level) : 0);
+
+ return array.join(".");
+};
+
+export default TerrainTile;
diff --git a/web/test/WebWorldWind/src/globe/TerrainTileList.js b/web/test/WebWorldWind/src/globe/TerrainTileList.js
new file mode 100644
index 00000000..4ced1357
--- /dev/null
+++ b/web/test/WebWorldWind/src/globe/TerrainTileList.js
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports TerrainTileList
+ */
+import ArgumentError from '../error/ArgumentError';
+import Logger from '../util/Logger';
+import Sector from '../geom/Sector';
+
+
+/**
+ * Constructs a terrain tile list, a container for terrain tiles that also has a tessellator and a sector
+ * associated with it.
+ * @alias TerrainTileList
+ * @constructor
+ * @classdesc Represents a portion of a globe's terrain.
+ * @param {Tessellator} tessellator The tessellator that created this terrain tile list.
+ *
+ */
+function TerrainTileList(tessellator) {
+ if (!tessellator) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "TerrainTileList", "TerrainTileList", "missingTessellator"));
+ }
+ this.tessellator = tessellator;
+ this.sector = null;
+ this.tileArray = [];
+}
+
+Object.defineProperties(TerrainTileList.prototype, {
+ /**
+ * The number of terrain tiles in this terrain tile list.
+ * @memberof TerrainTileList.prototype
+ * @readonly
+ * @type {Number}
+ */
+ length: {
+ get: function () {
+ return this.tileArray.length;
+ }
+ }
+});
+
+TerrainTileList.prototype.addTile = function (tile) {
+ if (!tile) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "TerrainTileList", "addTile", "missingTile"));
+ }
+
+ if (this.tileArray.indexOf(tile) == -1) {
+ this.tileArray.push(tile);
+
+ if (!this.sector) {
+ this.sector = new Sector(0, 0, 0, 0);
+ this.sector.copy(tile.sector);
+ } else {
+ this.sector.union(tile.sector);
+ }
+ }
+};
+
+TerrainTileList.prototype.removeAllTiles = function () {
+ this.tileArray = [];
+ this.sector = null;
+};
+
+export default TerrainTileList;
diff --git a/web/test/WebWorldWind/src/globe/Tessellator.js b/web/test/WebWorldWind/src/globe/Tessellator.js
new file mode 100644
index 00000000..3b9a740b
--- /dev/null
+++ b/web/test/WebWorldWind/src/globe/Tessellator.js
@@ -0,0 +1,1528 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports Tessellator
+ */
+import Angle from '../geom/Angle';
+import ArgumentError from '../error/ArgumentError';
+import BasicProgram from '../shaders/BasicProgram';
+import LevelSet from '../util/LevelSet';
+import Location from '../geom/Location';
+import Logger from '../util/Logger';
+import Matrix from '../geom/Matrix';
+import MemoryCache from '../cache/MemoryCache';
+import PickedObject from '../pick/PickedObject';
+import Position from '../geom/Position';
+import Sector from '../geom/Sector';
+import Terrain from '../globe/Terrain';
+import TerrainTile from '../globe/TerrainTile';
+import TerrainTileList from '../globe/TerrainTileList';
+import Tile from '../util/Tile';
+import WWMath from '../util/WWMath';
+import WWUtil from '../util/WWUtil';
+
+
+/**
+ * Constructs a Tessellator.
+ * @alias Tessellator
+ * @constructor
+ * @classdesc Provides terrain tessellation for a globe.
+ */
+function Tessellator() {
+ // Parameterize top level subdivision in one place.
+
+ // TilesInTopLevel describes the most coarse tile structure.
+ this.numRowsTilesInTopLevel = 1; // baseline: 4
+ this.numColumnsTilesInTopLevel = 1; // baseline: 8
+
+ // The maximum number of levels that will ever be tessellated.
+ this.maximumSubdivisionDepth = 16; // baseline: 15
+
+ // tileWidth, tileHeight - the number of subdivisions a single tile has; this determines the sampling grid.
+ this.tileWidth = 256; // baseline: 32
+ this.tileHeight = 256; // baseline: 32
+
+ /**
+ * Controls the level of detail switching for this layer. The next highest resolution level is
+ * used when an elevation tile's cell size is greater than this number of pixels, up to the maximum
+ * resolution of the elevation model.
+ * @type {Number}
+ * @default 1.75
+ */
+ this.detailControl = 40;
+
+ this.levels = new LevelSet(
+ Sector.FULL_SPHERE,
+ new Location(
+ 360 / this.numRowsTilesInTopLevel,
+ 360 / this.numColumnsTilesInTopLevel),
+ this.maximumSubdivisionDepth,
+ this.tileWidth,
+ this.tileHeight);
+
+ this.topLevelTiles = {};
+ this.currentTiles = new TerrainTileList(this);
+
+ this.tileCache = new MemoryCache(5000000, 4000000); // Holds 316 32x32 tiles.
+
+ this.elevationTimestamp = undefined;
+ this.lastModelViewProjection = Matrix.fromIdentity();
+
+ this.vertexPointLocation = -1;
+ this.vertexTexCoordLocation = -1;
+
+ this.texCoords = null;
+ this.texCoordVboCacheKey = 'global_tex_coords';
+
+ this.indices = null;
+ this.indicesVboCacheKey = 'global_indices';
+
+ this.baseIndices = null;
+ this.baseIndicesOffset = null;
+ this.numBaseIndices = null;
+
+ this.indicesNorth = null;
+ this.indicesNorthOffset = null;
+ this.numIndicesNorth = null;
+
+ this.indicesSouth = null;
+ this.indicesSouthOffset = null;
+ this.numIndicesSouth = null;
+
+ this.indicesWest = null;
+ this.indicesWestOffset = null;
+ this.numIndicesWest = null;
+
+ this.indicesEast = null;
+ this.indicesEastOffset = null;
+ this.numIndicesEast = null;
+
+ this.indicesLoresNorth = null;
+ this.indicesLoresNorthOffset = null;
+ this.numIndicesLoresNorth = null;
+
+ this.indicesLoresSouth = null;
+ this.indicesLoresSouthOffset = null;
+ this.numIndicesLoresSouth = null;
+
+ this.indicesLoresWest = null;
+ this.indicesLoresWestOffset = null;
+ this.numIndicesLoresWest = null;
+
+ this.indicesLoresEast = null;
+ this.indicesLoresEastOffset = null;
+ this.numIndicesLoresEast = null;
+
+ this.outlineIndicesOffset = null;
+ this.numOutlineIndices = null;
+
+ this.wireframeIndicesOffset = null;
+ this.numWireframeIndices = null;
+
+ this.scratchMatrix = Matrix.fromIdentity();
+ this.scratchElevations = null;
+ this.scratchPrevElevations = null;
+
+ this.corners = {};
+ this.tiles = [];
+}
+
+/**
+ * Creates the visible terrain of the globe associated with the current draw context.
+ * @param {DrawContext} dc The draw context.
+ * @returns {Terrain} The computed terrain, or null if terrain could not be computed.
+ * @throws {ArgumentError} If the dc is null or undefined.
+ */
+Tessellator.prototype.tessellate = function (dc) {
+ if (!dc) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Tessellator", "tessellate", "missingDC"));
+ }
+
+ var lastElevationsChange = dc.globe.elevationTimestamp();
+ if (this.lastGlobeStateKey === dc.globeStateKey
+ && this.lastVerticalExaggeration === dc.verticalExaggeration
+ && this.elevationTimestamp === lastElevationsChange
+ && dc.modelviewProjection.equals(this.lastModelViewProjection)) {
+
+ return this.lastTerrain;
+ }
+
+ this.lastModelViewProjection.copy(dc.modelviewProjection);
+ this.lastGlobeStateKey = dc.globeStateKey;
+ this.elevationTimestamp = lastElevationsChange;
+ this.lastVerticalExaggeration = dc.verticalExaggeration;
+
+ this.currentTiles.removeAllTiles();
+
+ if (!this.topLevelTiles[dc.globeStateKey] || this.topLevelTiles[dc.globeStateKey].length == 0) {
+ this.createTopLevelTiles(dc);
+ }
+
+ this.corners = {};
+ this.tiles = [];
+
+ for (var index = 0, len = this.topLevelTiles[dc.globeStateKey].length; index < len; index += 1) {
+ var tile = this.topLevelTiles[dc.globeStateKey][index];
+
+ tile.update(dc);
+
+ if (this.isTileVisible(dc, tile)) {
+ this.addTileOrDescendants(dc, tile);
+ }
+ }
+
+ this.refineNeighbors(dc);
+ this.finishTessellating(dc);
+
+ this.lastTerrain = this.currentTiles.length === 0 ? null
+ : new Terrain(dc.globe, this, this.currentTiles, dc.verticalExaggeration);
+
+ return this.lastTerrain;
+};
+
+Tessellator.prototype.createTile = function (tileSector, level, row, column) {
+ if (!tileSector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "constructor", "missingSector"));
+ }
+
+ if (!level) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "constructor",
+ "The specified level is null or undefined."));
+ }
+
+ if (row < 0 || column < 0) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "constructor",
+ "The specified row or column is less than zero."));
+ }
+
+ return new TerrainTile(tileSector, level, row, column);
+};
+
+/**
+ * Initializes rendering state to draw a succession of terrain tiles.
+ * @param {DrawContext} dc The draw context.
+ */
+Tessellator.prototype.beginRendering = function (dc) {
+ var program = dc.currentProgram; // use the current program; the caller configures other program state
+ if (!program) {
+ Logger.logMessage(Logger.LEVEL_INFO, "Tessellator", "beginRendering", "Current Program is empty");
+ return;
+ }
+
+ this.buildSharedGeometry();
+ this.cacheSharedGeometryVBOs(dc);
+
+ var gl = dc.currentGlContext,
+ gpuResourceCache = dc.gpuResourceCache;
+
+ // Keep track of the program's attribute locations. The tessellator does not know which program the caller has
+ // bound, and therefore must look up the location of attributes by name.
+ this.vertexPointLocation = program.attributeLocation(gl, "vertexPoint");
+ this.vertexTexCoordLocation = program.attributeLocation(gl, "vertexTexCoord");
+ gl.enableVertexAttribArray(this.vertexPointLocation);
+
+ if (this.vertexTexCoordLocation >= 0) { // location of vertexTexCoord attribute is -1 when the basic program is bound
+ var texCoordVbo = gpuResourceCache.resourceForKey(this.texCoordVboCacheKey);
+ gl.bindBuffer(gl.ARRAY_BUFFER, texCoordVbo);
+ gl.vertexAttribPointer(this.vertexTexCoordLocation, 2, gl.FLOAT, false, 0, 0);
+ gl.enableVertexAttribArray(this.vertexTexCoordLocation);
+ }
+
+ var indicesVbo = gpuResourceCache.resourceForKey(this.indicesVboCacheKey);
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indicesVbo);
+
+};
+
+/**
+ * Restores rendering state after drawing a succession of terrain tiles.
+ * @param {DrawContext} dc The draw context.
+ */
+Tessellator.prototype.endRendering = function (dc) {
+ var gl = dc.currentGlContext;
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
+
+ // Restore the global OpenGL vertex attribute array state.
+ if (this.vertexPointLocation >= 0) {
+ gl.disableVertexAttribArray(this.vertexPointLocation);
+ }
+
+ if (this.vertexTexCoordLocation >= 0) { // location of vertexTexCoord attribute is -1 when the basic program is bound
+ gl.disableVertexAttribArray(this.vertexTexCoordLocation);
+ }
+};
+
+/**
+ * Initializes rendering state for drawing a specified terrain tile.
+ * @param {DrawContext} dc The draw context.
+ * @param {TerrainTile} terrainTile The terrain tile subsequently drawn via this tessellator's render function.
+ * @throws {ArgumentError} If the specified tile is null or undefined.
+ */
+Tessellator.prototype.beginRenderingTile = function (dc, terrainTile) {
+ if (!terrainTile) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Tessellator", "beginRenderingTile", "missingTile"));
+ }
+
+ var gl = dc.currentGlContext,
+ gpuResourceCache = dc.gpuResourceCache;
+
+ this.scratchMatrix.setToMultiply(dc.modelviewProjection, terrainTile.transformationMatrix);
+ dc.currentProgram.loadModelviewProjection(gl, this.scratchMatrix);
+
+ var vboCacheKey = dc.globeStateKey + terrainTile.tileKey,
+ vbo = gpuResourceCache.resourceForKey(vboCacheKey);
+ if (!vbo) {
+ vbo = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
+ gl.bufferData(gl.ARRAY_BUFFER, terrainTile.points, gl.STATIC_DRAW);
+ dc.frameStatistics.incrementVboLoadCount(1);
+ gpuResourceCache.putResource(vboCacheKey, vbo, terrainTile.points.length * 4);
+ terrainTile.pointsVboStateKey = terrainTile.pointsStateKey;
+ }
+ else if (terrainTile.pointsVboStateKey != terrainTile.pointsStateKey) {
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
+ gl.bufferSubData(gl.ARRAY_BUFFER, 0, terrainTile.points);
+ terrainTile.pointsVboStateKey = terrainTile.pointsStateKey;
+ }
+ else {
+ dc.currentGlContext.bindBuffer(gl.ARRAY_BUFFER, vbo);
+ }
+
+ gl.vertexAttribPointer(this.vertexPointLocation, 3, gl.FLOAT, false, 0, 0);
+};
+
+/**
+ * Restores rendering state after drawing the most recent tile specified to
+ * [beginRenderingTile]{@link Tessellator#beginRenderingTile}.
+ * @param {DrawContext} dc The draw context.
+ * @param {TerrainTile} terrainTile The terrain tile most recently rendered.
+ * @throws {ArgumentError} If the specified tile is null or undefined.
+ */
+Tessellator.prototype.endRenderingTile = function (dc, terrainTile) {
+ // Intentionally empty until there's some reason to add code here.
+};
+
+/**
+ * Renders a specified terrain tile.
+ * @param {DrawContext} dc The draw context.
+ * @param {TerrainTile} terrainTile The terrain tile to render.
+ * @throws {ArgumentError} If the specified tile is null or undefined.
+ */
+Tessellator.prototype.renderTile = function (dc, terrainTile) {
+ if (!terrainTile) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Tessellator", "renderTile", "missingTile"));
+ }
+
+ var gl = dc.currentGlContext,
+ prim = gl.TRIANGLE_STRIP; // replace TRIANGLE_STRIP with LINE_STRIP to debug borders
+
+ /*
+ * Indices order in the buffer:
+ *
+ * base indices
+ *
+ * north border
+ * south border
+ * west border
+ * east border
+ *
+ * north lores
+ * south lores
+ * west lores
+ * east lores
+ *
+ * wireframe
+ * outline
+ */
+
+ gl.drawElements(
+ prim,
+ this.numBaseIndices,
+ gl.UNSIGNED_SHORT,
+ this.baseIndicesOffset * 2);
+
+ var level = terrainTile.level,
+ neighborLevel;
+
+ neighborLevel = terrainTile.neighborLevel(WorldWind.NORTH);
+ if (neighborLevel && neighborLevel.compare(level) < 0) {
+ gl.drawElements(
+ prim,
+ this.numIndicesLoresNorth,
+ gl.UNSIGNED_SHORT,
+ this.indicesLoresNorthOffset * 2);
+ }
+ else {
+ gl.drawElements(
+ prim,
+ this.numIndicesNorth,
+ gl.UNSIGNED_SHORT,
+ this.indicesNorthOffset * 2);
+ }
+
+ neighborLevel = terrainTile.neighborLevel(WorldWind.SOUTH);
+ if (neighborLevel && neighborLevel.compare(level) < 0) {
+ gl.drawElements(
+ prim,
+ this.numIndicesLoresSouth,
+ gl.UNSIGNED_SHORT,
+ this.indicesLoresSouthOffset * 2);
+ }
+ else {
+ gl.drawElements(
+ prim,
+ this.numIndicesSouth,
+ gl.UNSIGNED_SHORT,
+ this.indicesSouthOffset * 2);
+ }
+
+ neighborLevel = terrainTile.neighborLevel(WorldWind.WEST);
+ if (neighborLevel && neighborLevel.compare(level) < 0) {
+ gl.drawElements(
+ prim,
+ this.numIndicesLoresWest,
+ gl.UNSIGNED_SHORT,
+ this.indicesLoresWestOffset * 2);
+ }
+ else {
+ gl.drawElements(
+ prim,
+ this.numIndicesWest,
+ gl.UNSIGNED_SHORT,
+ this.indicesWestOffset * 2);
+ }
+
+ neighborLevel = terrainTile.neighborLevel(WorldWind.EAST);
+ if (neighborLevel && neighborLevel.compare(level) < 0) {
+ gl.drawElements(
+ prim,
+ this.numIndicesLoresEast,
+ gl.UNSIGNED_SHORT,
+ this.indicesLoresEastOffset * 2);
+ }
+ else {
+ gl.drawElements(
+ prim,
+ this.numIndicesEast,
+ gl.UNSIGNED_SHORT,
+ this.indicesEastOffset * 2);
+ }
+};
+
+/**
+ * Draws outlines of the triangles composing the tile.
+ * @param {DrawContext} dc The current draw context.
+ * @param {TerrainTile} terrainTile The tile to draw.
+ * @throws {ArgumentError} If the specified tile is null or undefined.
+ */
+Tessellator.prototype.renderWireframeTile = function (dc, terrainTile) {
+ if (!terrainTile) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Tessellator", "renderWireframeTile", "missingTile"));
+ }
+
+ var gl = dc.currentGlContext;
+
+ // Must turn off texture coordinates, which were turned on in beginRendering.
+ if (this.vertexTexCoordLocation >= 0) {
+ gl.disableVertexAttribArray(this.vertexTexCoordLocation);
+ }
+
+ gl.drawElements(
+ gl.LINES,
+ this.numWireframeIndices,
+ gl.UNSIGNED_SHORT,
+ this.wireframeIndicesOffset * 2);
+};
+
+/**
+ * Draws the outer boundary of a specified terrain tile.
+ * @param {DrawContext} dc The current draw context.
+ * @param {TerrainTile} terrainTile The tile whose outer boundary to draw.
+ * @throws {ArgumentError} If the specified tile is null or undefined.
+ */
+Tessellator.prototype.renderTileOutline = function (dc, terrainTile) {
+ if (!terrainTile) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Tessellator", "renderTileOutline", "missingTile"));
+ }
+
+ var gl = dc.currentGlContext;
+
+ // Must turn off texture coordinates, which were turned on in beginRendering.
+ if (this.vertexTexCoordLocation >= 0) {
+ gl.disableVertexAttribArray(this.vertexTexCoordLocation);
+ }
+
+ gl.drawElements(
+ gl.LINE_LOOP,
+ this.numOutlineIndices,
+ gl.UNSIGNED_SHORT,
+ this.outlineIndicesOffset * 2);
+};
+
+/**
+ * Causes this terrain to perform the picking operations on the specified tiles, as appropriate for the draw
+ * context's pick settings. Normally, this draws the terrain in a unique pick color and computes the picked
+ * terrain position. When the draw context is set to region picking mode, this omits the computation of a picked
+ * terrain position.
+ * @param {DrawContext} dc The current draw context.
+ * @param {Array} tileList The list of tiles to pick.
+ * @param {Object} pickDelegate Indicates the object to use as the picked object's userObject.
+ * If null, then this tessellator is used as the userObject.
+ * @throws {ArgumentError} If either the draw context or the tile list are null or undefined.
+ */
+Tessellator.prototype.pick = function (dc, tileList, pickDelegate) {
+ if (!dc) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Tessellator", "pick", "missingDc"));
+ }
+
+ if (!tileList) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Tessellator", "pick", "missingList"));
+ }
+
+ var color = null,
+ userObject = pickDelegate || this,
+ position = new Position(0, 0, 0),
+ pickableTiles = [];
+
+ // Assemble a list of tiles that intersect the pick frustum. This eliminates unnecessary work for tiles that
+ // do not contribute to the pick result.
+ for (var i = 0, len = tileList.length; i < len; i++) {
+ var tile = tileList[i];
+ if (tile.extent.intersectsFrustum(dc.pickFrustum)) {
+ pickableTiles.push(tile);
+ }
+ }
+
+ // Draw the pickable tiles in a unique pick color. Suppress this step when picking the terrain only. In this
+ // case drawing to the pick framebuffer is unnecessary.
+ if (!dc.pickTerrainOnly) {
+ color = dc.uniquePickColor();
+ this.drawPickTiles(dc, pickableTiles, color);
+ }
+
+ // Determine the terrain position at the pick point. If the terrain is picked, add a corresponding picked
+ // object to the draw context. Suppress this step in region picking mode.
+ if (!dc.regionPicking) {
+ var ray = dc.pickRay.clone(), // Cloning the pick ray is necessary here due to the fact that Tesselator.computeIntersections modifies ray
+ point = this.computeNearestIntersection(ray, pickableTiles);
+
+ if (point) {
+ dc.globe.computePositionFromPoint(point[0], point[1], point[2], position);
+ position.altitude = dc.globe.elevationAtLocation(position.latitude, position.longitude);
+ dc.addPickedObject(new PickedObject(color, userObject, position, null, true));
+ }
+ }
+};
+
+// Internal function. Intentionally not documented.
+Tessellator.prototype.drawPickTiles = function (dc, tileList, color) {
+ var gl = dc.currentGlContext;
+
+ try {
+ dc.findAndBindProgram(BasicProgram);
+ dc.currentProgram.loadColor(gl, color);
+ this.beginRendering(dc);
+
+ for (var i = 0, len = tileList.length; i < len; i++) {
+ var tile = tileList[i];
+ this.beginRenderingTile(dc, tile);
+ this.renderTile(dc, tile);
+ this.endRenderingTile(dc, tile);
+ }
+ } finally {
+ this.endRendering(dc);
+ }
+};
+
+// Internal function. Intentionally not documented.
+Tessellator.prototype.computeNearestIntersection = function (line, tileList) {
+ // Compute all intersections between the specified line and tile list.
+ var results = [];
+ for (var i = 0, len = tileList.length; i < len; i++) {
+ this.computeIntersections(line, tileList[i], results);
+ }
+
+ if (results.length == 0) {
+ return null; // no intersection
+ } else {
+ // Find and return the intersection nearest to the line's origin.
+ var minDistance = Number.POSITIVE_INFINITY,
+ minIndex;
+ for (i = 0, len = results.length; i < len; i++) {
+ var distance = line.origin.distanceToSquared(results[i]);
+ if (minDistance > distance) {
+ minDistance = distance;
+ minIndex = i;
+ }
+ }
+
+ return results[minIndex];
+ }
+};
+
+// Internal function. Intentionally not documented.
+Tessellator.prototype.computeIntersections = function (line, tile, results) {
+ var level = tile.level,
+ neighborLevel,
+ points = tile.points,
+ elements,
+ firstResult = results.length;
+
+ // Translate the line from model coordinates to tile local coordinates.
+ line.origin.subtract(tile.referencePoint);
+
+ // Assemble the shared tile index geometry. This initializes the index properties used below.
+ this.buildSharedGeometry(tile);
+
+ // Compute any intersections with the tile's interior triangles..
+ elements = this.baseIndices;
+ WWMath.computeTriStripIntersections(line, points, elements, results);
+
+ // Compute any intersections with the tile's south border triangles.
+ neighborLevel = tile.neighborLevel(WorldWind.SOUTH);
+ elements = neighborLevel && neighborLevel.compare(level) < 0 ? this.indicesLoresSouth : this.indicesSouth;
+ WWMath.computeTriStripIntersections(line, points, elements, results);
+
+ // Compute any intersections with the tile's west border triangles.
+ neighborLevel = tile.neighborLevel(WorldWind.WEST);
+ elements = neighborLevel && neighborLevel.compare(level) < 0 ? this.indicesLoresWest : this.indicesWest;
+ WWMath.computeTriStripIntersections(line, points, elements, results);
+
+ // Compute any intersections with the tile's east border triangles.
+ neighborLevel = tile.neighborLevel(WorldWind.EAST);
+ elements = neighborLevel && neighborLevel.compare(level) < 0 ? this.indicesLoresEast : this.indicesEast;
+ WWMath.computeTriStripIntersections(line, points, elements, results);
+
+ // Compute any intersections with the tile's north border triangles.
+ neighborLevel = tile.neighborLevel(WorldWind.NORTH);
+ elements = neighborLevel && neighborLevel.compare(level) < 0 ? this.indicesLoresNorth : this.indicesNorth;
+ WWMath.computeTriStripIntersections(line, points, elements, results);
+
+ // Translate the line and the intersection results from tile local coordinates to model coordinates.
+ line.origin.add(tile.referencePoint);
+ for (var i = firstResult, len = results.length; i < len; i++) {
+ results[i].add(tile.referencePoint);
+ }
+};
+
+/***********************************************************************
+ * Internal methods - assume that arguments have been validated already.
+ ***********************************************************************/
+
+Tessellator.prototype.createTopLevelTiles = function (dc) {
+ this.topLevelTiles[dc.globeStateKey] = [];
+ Tile.createTilesForLevel(this.levels.firstLevel(), this, this.topLevelTiles[dc.globeStateKey]);
+};
+
+Tessellator.prototype.addTileOrDescendants = function (dc, tile) {
+ if (this.tileMeetsRenderCriteria(dc, tile)) {
+ this.addTile(dc, tile);
+ return;
+ }
+
+ this.addTileDescendants(dc, tile);
+};
+
+Tessellator.prototype.addTileDescendants = function (dc, tile) {
+ var nextLevel = tile.level.nextLevel();
+ var subTiles = tile.subdivideToCache(nextLevel, this, this.tileCache);
+ for (var index = 0; index < subTiles.length; index += 1) {
+ var child = subTiles[index];
+
+ child.update(dc);
+
+ if (this.levels.sector.intersects(child.sector) && this.isTileVisible(dc, child)) {
+ this.addTileOrDescendants(dc, child);
+ }
+ }
+};
+
+Tessellator.prototype.addTile = function (dc, tile) {
+ // Insert tile at index idx.
+ var idx = this.tiles.length;
+ this.tiles.push(tile);
+
+ // Insert tile into corner data collection for later LOD neighbor analysis.
+ var sector = tile.sector;
+
+ // Corners of the tile.
+ var neTileCorner = [sector.maxLatitude, sector.maxLongitude].toString(),
+ seTileCorner = [sector.minLatitude, sector.maxLongitude].toString(),
+ nwTileCorner = [sector.maxLatitude, sector.minLongitude].toString(),
+ swTileCorner = [sector.minLatitude, sector.minLongitude].toString(),
+ corner;
+
+ corner = this.corners[swTileCorner];
+ if (!corner) {
+ this.corners[swTileCorner] = { 'sw': idx }; //corner;
+ }
+ else {
+ // assert(!corner.sw, "sw already defined");
+ corner.sw = idx;
+ }
+
+ corner = this.corners[nwTileCorner];
+ if (!corner) {
+ this.corners[nwTileCorner] = { 'nw': idx };
+ }
+ else {
+ // assert(!corner.nw, "nw already defined");
+ corner.nw = idx;
+ }
+
+ corner = this.corners[seTileCorner];
+ if (!corner) {
+ this.corners[seTileCorner] = { 'se': idx };
+ }
+ else {
+ // assert(!corver.se, "se already defined");
+ corner.se = idx;
+ }
+
+ corner = this.corners[neTileCorner];
+ if (!corner) {
+ this.corners[neTileCorner] = { 'ne': idx };
+ }
+ else {
+ //assert(!corner.ne, "ne already defined");
+ corner.ne = idx;
+ }
+};
+
+Tessellator.prototype.refineNeighbors = function (dc) {
+ var tileRefinementSet = {};
+
+ for (var idx = 0, len = this.tiles.length; idx < len; idx += 1) {
+ var tile = this.tiles[idx],
+ levelNumber = tile.level.levelNumber,
+ sector = tile.sector,
+ corner,
+ neighbor,
+ idx,
+ len;
+
+ // Corners of the tile.
+ var neTileCorner = [sector.maxLatitude, sector.maxLongitude].toString(),
+ seTileCorner = [sector.minLatitude, sector.maxLongitude].toString(),
+ nwTileCorner = [sector.maxLatitude, sector.minLongitude].toString(),
+ swTileCorner = [sector.minLatitude, sector.minLongitude].toString();
+
+ corner = this.corners[neTileCorner];
+ // assert(corner, "northeast corner not found");
+ if (corner.hasOwnProperty('se')) {
+ neighbor = corner.se;
+ if (this.tiles[neighbor].level.levelNumber < levelNumber - 1) {
+ if (!tileRefinementSet[neighbor]) {
+ tileRefinementSet[neighbor] = true;
+ }
+ }
+ }
+ if (corner.hasOwnProperty('nw')) {
+ neighbor = corner.nw;
+ if (this.tiles[neighbor].level.levelNumber < levelNumber - 1) {
+ if (!tileRefinementSet[neighbor]) {
+ tileRefinementSet[neighbor] = true;
+ }
+ }
+ }
+
+ corner = this.corners[seTileCorner];
+ // assert(corner, "southeast corner not found");
+ if (corner.hasOwnProperty('ne')) {
+ neighbor = corner.ne;
+ if (this.tiles[neighbor].level.levelNumber < levelNumber - 1) {
+ if (!tileRefinementSet[neighbor]) {
+ tileRefinementSet[neighbor] = true;
+ }
+ }
+ }
+ if (corner.hasOwnProperty('sw')) {
+ neighbor = corner.sw;
+ if (this.tiles[neighbor].level.levelNumber < levelNumber - 1) {
+ if (!tileRefinementSet[neighbor]) {
+ tileRefinementSet[neighbor] = true;
+ }
+ }
+ }
+
+ corner = this.corners[nwTileCorner];
+ // assert(corner, "northwest corner not found");
+ if (corner.hasOwnProperty('ne')) {
+ neighbor = corner.ne;
+ if (this.tiles[neighbor].level.levelNumber < levelNumber - 1) {
+ if (!tileRefinementSet[neighbor]) {
+ tileRefinementSet[neighbor] = true;
+ }
+ }
+ }
+ if (corner.hasOwnProperty('sw')) {
+ neighbor = corner.sw;
+ if (this.tiles[neighbor].level.levelNumber < levelNumber - 1) {
+ if (!tileRefinementSet[neighbor]) {
+ tileRefinementSet[neighbor] = true;
+ }
+ }
+ }
+
+ corner = this.corners[swTileCorner];
+ // assert(corner, "southwest corner not found");
+ if (corner.hasOwnProperty('se')) {
+ neighbor = corner.se;
+ if (this.tiles[neighbor].level.levelNumber < levelNumber - 1) {
+ if (!tileRefinementSet[neighbor]) {
+ tileRefinementSet[neighbor] = true;
+ }
+ }
+ }
+ if (corner.hasOwnProperty('nw')) {
+ neighbor = corner.nw;
+ if (this.tiles[neighbor].level.levelNumber < levelNumber - 1) {
+ if (!tileRefinementSet[neighbor]) {
+ tileRefinementSet[neighbor] = true;
+ }
+ }
+ }
+ }
+
+ // Partition tiles into those requiring refinement and those that don't need refinement.
+ var tilesNeedingRefinement = [],
+ tilesNotNeedingRefinement = [];
+ for (idx = 0, len = this.tiles.length; idx < len; idx += 1) {
+ tile = this.tiles[idx];
+ if (tileRefinementSet[idx]) {
+ tilesNeedingRefinement.push(tile);
+ }
+ else {
+ tilesNotNeedingRefinement.push(tile);
+ }
+ }
+
+ // When tiles need refinement, recur.
+ if (tilesNeedingRefinement.length > 0) {
+ // Reset refinement state.
+ this.tiles = [];
+ this.corners = {};
+
+ // For tiles that don't need refinement, simply add the tile.
+ for (idx = 0, len = tilesNotNeedingRefinement.length; idx < len; idx += 1) {
+ tile = tilesNotNeedingRefinement[idx];
+
+ this.addTile(dc, tile);
+ }
+
+ // For tiles that do need refinement, subdivide the tile and add its descendants.
+ for (idx = 0, len = tilesNeedingRefinement.length; idx < len; idx += 1) {
+ var tile = tilesNeedingRefinement[idx];
+
+ this.addTileDescendants(dc, tile);
+ }
+
+ // Recur.
+ this.refineNeighbors(dc);
+ }
+};
+
+Tessellator.prototype.finishTessellating = function (dc) {
+ for (var idx = 0, len = this.tiles.length; idx < len; idx += 1) {
+ var tile = this.tiles[idx];
+ this.setNeighbors(tile);
+ this.regenerateTileGeometryIfNeeded(dc, tile);
+ this.currentTiles.addTile(tile);
+ }
+};
+
+Tessellator.prototype.setNeighbors = function (tile) {
+ var sector = tile.sector;
+
+ // Corners of the tile.
+ var neTileCorner = [sector.maxLatitude, sector.maxLongitude].toString(),
+ seTileCorner = [sector.minLatitude, sector.maxLongitude].toString(),
+ nwTileCorner = [sector.maxLatitude, sector.minLongitude].toString(),
+ swTileCorner = [sector.minLatitude, sector.minLongitude].toString();
+
+ var neCorner = this.corners[neTileCorner],
+ seCorner = this.corners[seTileCorner],
+ nwCorner = this.corners[nwTileCorner],
+ swCorner = this.corners[swTileCorner];
+
+ var northIdx = -1, // neCorner.hasOwnProperty('se') ? neCorner.se : nwCorner.hasOwnProperty('sw') ? nwCorner.sw : -1,
+ southIdx = -1, // seCorner.hasOwnProperty('ne') ? seCorner.ne : swCorner.hasOwnProperty('nw') ? swCorner.nw : -1,
+ eastIdx = -1, // neCorner.hasOwnProperty('nw') ? neCorner.nw : seCorner.hasOwnProperty('sw') ? seCorner.sw : -1,
+ westIdx = -1; //nwCorner.hasOwnProperty('ne') ? nwCorner.ne : swCorner.hasOwnProperty('se') ? swCorner.se : -1;
+
+ if (neCorner.hasOwnProperty('se')) {
+ northIdx = neCorner.se;
+ }
+ else if (nwCorner.hasOwnProperty('sw')) {
+ northIdx = nwCorner.sw;
+ }
+
+ if (seCorner.hasOwnProperty('ne')) {
+ southIdx = seCorner.ne;
+ }
+ else if (swCorner.hasOwnProperty('nw')) {
+ southIdx = swCorner.nw;
+ }
+
+ if (neCorner.hasOwnProperty('nw')) {
+ eastIdx = neCorner.nw;
+ }
+ else if (seCorner.hasOwnProperty('sw')) {
+ eastIdx = seCorner.sw;
+ }
+
+ if (nwCorner.hasOwnProperty('ne')) {
+ westIdx = nwCorner.ne;
+ }
+ else if (swCorner.hasOwnProperty('se')) {
+ westIdx = swCorner.se;
+ }
+
+ tile.setNeighborLevel(WorldWind.NORTH, northIdx >= 0 ? this.tiles[northIdx].level : null);
+ tile.setNeighborLevel(WorldWind.SOUTH, southIdx >= 0 ? this.tiles[southIdx].level : null);
+ tile.setNeighborLevel(WorldWind.EAST, eastIdx >= 0 ? this.tiles[eastIdx].level : null);
+ tile.setNeighborLevel(WorldWind.WEST, westIdx >= 0 ? this.tiles[westIdx].level : null);
+};
+
+Tessellator.prototype.isTileVisible = function (dc, tile) {
+ if (dc.globe.projectionLimits && !tile.sector.overlaps(dc.globe.projectionLimits)) {
+ return false;
+ }
+
+ return tile.extent.intersectsFrustum(dc.frustumInModelCoordinates);
+};
+
+Tessellator.prototype.tileMeetsRenderCriteria = function (dc, tile) {
+ var s = this.detailControl;
+ var lat = WWMath.mercatorLat(75);
+ if (tile.sector.minLatitude >= lat || tile.sector.maxLatitude <= -lat) {
+ s *= 2;
+ }
+ return tile.level.isLastLevel() || !tile.mustSubdivide(dc, s);
+};
+
+Tessellator.prototype.regenerateTileGeometryIfNeeded = function (dc, tile) {
+ var stateKey = dc.globeStateKey + tile.stateKey + dc.verticalExaggeration;
+
+ if (!tile.points || tile.pointsStateKey != stateKey) {
+ this.regenerateTileGeometry(dc, tile);
+ tile.pointsStateKey = stateKey;
+ }
+};
+
+/**
+ * Internal use only.
+ * TODO: Remove this function when Tessellator and ElevationModel are refactored
+ * Artificially calculates an adjusted target resolution for the given texel size to more
+ * optimally select elevation coverages until later refactoring.
+ * @returns {Number} An adjusted target resolution in degrees.
+ * @ignore
+ */
+Tessellator.prototype.coverageTargetResolution = function (texelSize) {
+ return texelSize * Angle.RADIANS_TO_DEGREES;
+};
+
+Tessellator.prototype.regenerateTileGeometry = function (dc, tile) {
+ var numLat = tile.tileHeight + 1, // num points in each dimension is 1 more than the number of tile cells
+ numLon = tile.tileWidth + 1,
+ refPoint = tile.referencePoint,
+ elevations = this.scratchElevations;
+
+ // Allocate space for the tile's elevations.
+ if (!elevations) {
+ elevations = new Float64Array(numLat * numLon);
+ this.scratchElevations = elevations;
+ }
+
+ // Allocate space for the tile's Cartesian coordinates.
+ if (!tile.points) {
+ tile.points = new Float32Array(numLat * numLon * 3);
+ }
+
+ // Retrieve the elevations for all points in the tile.
+ WWUtil.fillArray(elevations, 0);
+
+ dc.globe.elevationsForGrid(tile.sector, numLat, numLon, this.coverageTargetResolution(tile.texelSize), elevations);
+
+ // Modify the elevations around the tile's border to match neighbors of lower resolution, if any.
+ if (this.mustAlignNeighborElevations(dc, tile)) {
+ this.alignNeighborElevations(dc, tile, elevations);
+ }
+
+ // Compute the tile's Cartesian coordinates relative to a local origin, called the reference point.
+ WWUtil.multiplyArray(elevations, dc.verticalExaggeration);
+ dc.globe.computePointsForGrid(tile.sector, numLat, numLon, elevations, refPoint, tile.points);
+
+ // Establish a transform that is used later to move the tile coordinates into place relative to the globe.
+ tile.transformationMatrix.setTranslation(refPoint[0], refPoint[1], refPoint[2]);
+};
+
+Tessellator.prototype.mustAlignNeighborElevations = function (dc, tile) {
+ var level = tile.level,
+ northLevel = tile.neighborLevel(WorldWind.NORTH),
+ southLevel = tile.neighborLevel(WorldWind.SOUTH),
+ eastLevel = tile.neighborLevel(WorldWind.EAST),
+ westLevel = tile.neighborLevel(WorldWind.WEST);
+
+ return northLevel && northLevel.compare(level) < 0 ||
+ southLevel && southLevel.compare(level) < 0 ||
+ eastLevel && eastLevel.compare(level) < 0 ||
+ westLevel && westLevel.compare(level) < 0;
+};
+
+Tessellator.prototype.alignNeighborElevations = function (dc, tile, elevations) {
+ var numLat = tile.tileHeight + 1, // num points in each dimension is 1 more than the number of tile cells
+ numLon = tile.tileWidth + 1,
+ level = tile.level,
+ prevNumLat = Math.floor(numLat / 2) + 1, // num prev level points is 1 more than 1/2 the number of cells
+ prevNumLon = Math.floor(numLon / 2) + 1,
+ prevLevel = level.previousLevel(),
+ prevElevations = this.scratchPrevElevations,
+ neighborLevel,
+ i, index, prevIndex;
+
+ // Allocate space for the previous level elevations.
+ if (!prevElevations) {
+ prevElevations = new Float64Array(prevNumLat * prevNumLon);
+ this.scratchPrevElevations = prevElevations;
+ }
+
+ // Retrieve the previous level elevations, using 1/2 the number of tile cells.
+ WWUtil.fillArray(prevElevations, 0);
+
+ dc.globe.elevationsForGrid(tile.sector, prevNumLat, prevNumLon, this.coverageTargetResolution(prevLevel.texelSize), prevElevations);
+
+ // Use previous level elevations along the north edge when the northern neighbor is lower resolution.
+ neighborLevel = tile.neighborLevel(WorldWind.NORTH);
+ if (neighborLevel && neighborLevel.compare(level) < 0) {
+ index = (numLat - 1) * numLon;
+ prevIndex = (prevNumLat - 1) * prevNumLon;
+ for (i = 0; i < prevNumLon; i++, index += 2, prevIndex += 1) {
+ elevations[index] = prevElevations[prevIndex];
+ if (i < prevNumLon - 1) {
+ elevations[index + 1] = 0.5 * (prevElevations[prevIndex] + prevElevations[prevIndex + 1]);
+ }
+ }
+ }
+
+ // Use previous level elevations along the south edge when the southern neighbor is lower resolution.
+ neighborLevel = tile.neighborLevel(WorldWind.SOUTH);
+ if (neighborLevel && neighborLevel.compare(level) < 0) {
+ index = 0;
+ prevIndex = 0;
+ for (i = 0; i < prevNumLon; i++, index += 2, prevIndex += 1) {
+ elevations[index] = prevElevations[prevIndex];
+ if (i < prevNumLon - 1) {
+ elevations[index + 1] = 0.5 * (prevElevations[prevIndex] + prevElevations[prevIndex + 1]);
+ }
+ }
+ }
+
+ // Use previous level elevations along the east edge when the eastern neighbor is lower resolution.
+ neighborLevel = tile.neighborLevel(WorldWind.EAST);
+ if (neighborLevel && neighborLevel.compare(level) < 0) {
+ index = numLon - 1;
+ prevIndex = prevNumLon - 1;
+ for (i = 0; i < prevNumLat; i++, index += 2 * numLon, prevIndex += prevNumLon) {
+ elevations[index] = prevElevations[prevIndex];
+ if (i < prevNumLat - 1) {
+ elevations[index + numLon] = 0.5 * (prevElevations[prevIndex] + prevElevations[prevIndex + prevNumLon]);
+ }
+ }
+ }
+
+ // Use previous level elevations along the west edge when the western neighbor is lower resolution.
+ neighborLevel = tile.neighborLevel(WorldWind.WEST);
+ if (neighborLevel && neighborLevel.compare(level) < 0) {
+ index = 0;
+ prevIndex = 0;
+ for (i = 0; i < prevNumLat; i++, index += 2 * numLon, prevIndex += prevNumLon) {
+ elevations[index] = prevElevations[prevIndex];
+ if (i < prevNumLat - 1) {
+ elevations[index + numLon] = 0.5 * (prevElevations[prevIndex] + prevElevations[prevIndex + prevNumLon]);
+ }
+ }
+ }
+};
+
+Tessellator.prototype.buildSharedGeometry = function () {
+ // TODO: put all indices into a single buffer
+ var tileWidth = this.levels.tileWidth,
+ tileHeight = this.levels.tileHeight;
+
+ if (!this.texCoords) {
+ this.buildTexCoords(tileWidth, tileHeight);
+ }
+
+ if (!this.indices) {
+ this.buildIndices(tileWidth, tileHeight);
+ }
+};
+
+Tessellator.prototype.buildTexCoords = function (tileWidth, tileHeight) {
+ var numCols = tileWidth + 1,
+ numRows = tileHeight + 1,
+ colDelta = 1 / tileWidth,
+ rowDelta = 1 / tileHeight,
+ buffer = new Float32Array(numCols * numRows * 2),
+ index = 0;
+
+ for (var row = 0, t = 0; row < numRows; row++, t += rowDelta) {
+ if (row == numRows - 1) {
+ t = 1; // explicitly set the last row coordinate to ensure alignment
+ }
+
+ for (var col = 0, s = 0; col < numCols; col++, s += colDelta) {
+ if (col == numCols - 1) {
+ s = 1; // explicitly set the last column coordinate to ensure alignment
+ }
+
+ buffer[index++] = s;
+ buffer[index++] = t;
+ }
+ }
+
+ this.texCoords = buffer;
+};
+
+Tessellator.prototype.buildIndices = function (tileWidth, tileHeight) {
+ var vertexIndex; // The index of the vertex in the sample grid.
+
+ // The number of vertices in each dimension is 1 more than the number of cells.
+ var numLatVertices = tileHeight + 1,
+ numLonVertices = tileWidth + 1,
+ latIndexMid = tileHeight / 2, // Assumption: tileHeight is even, so that there is a midpoint!
+ lonIndexMid = tileWidth / 2; // Assumption: tileWidth is even, so that there is a midpoint!
+
+ // Each vertex has two indices associated with it: the current vertex index and the index of the row.
+ // There are tileHeight rows.
+ // There are tileHeight + 2 columns
+ var numIndices = 2 * (numLatVertices - 3) * (numLonVertices - 2) + 2 * (numLatVertices - 3);
+ var indices = [];
+
+ // Inset core by one round of sub-tiles. Full grid is numLatVertices x numLonVertices. This must be used
+ // to address vertices in the core as well.
+ var index = 0;
+ for (var lonIndex = 1; lonIndex < numLonVertices - 2; lonIndex += 1) {
+ for (var latIndex = 1; latIndex < numLatVertices - 1; latIndex += 1) {
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+
+ // Create a triangle strip joining each adjacent column of vertices, starting in the top left corner and
+ // proceeding to the right. The first vertex starts with the left row of vertices and moves right to create a
+ // counterclockwise winding order.
+ indices[index++] = vertexIndex;
+ indices[index++] = vertexIndex + 1;
+ }
+
+ // Insert indices to create 2 degenerate triangles:
+ // one for the end of the current row, and
+ // one for the beginning of the next row.
+ indices[index++] = vertexIndex + 1;
+ vertexIndex = lonIndex + 1 + 1 * numLonVertices;
+ indices[index++] = vertexIndex;
+ }
+
+ this.baseIndicesOffset = indices.length - numIndices;
+ this.baseIndices = new Uint16Array(indices.slice(this.baseIndicesOffset));
+ this.numBaseIndices = numIndices;
+
+ // TODO: parameterize and refactor!!!!!
+ // Software engineering notes: There are patterns being used in the following code that should be abstracted.
+ // However, I suspect that the process of abstracting the patterns will result in as much code created
+ // as gets removed. YMMV. If JavaScript had a meta-programming (a.k.a., macro) facility, that code would be
+ // processed at "compile" time rather than "runtime". But it doesn't have such a facility that I know of.
+ //
+ // Patterns used:
+ // 0) Each tile has four borders: north, south, east, and west.
+ // 1) Counter-clockwise traversal around the outside results in clockwise meshes amendable to back-face elimination.
+ // 2) For each vertex on the exterior, there corresponds a vertex on the interior that creates a diagonal.
+ // 3) Each border construction is broken into three phases:
+ // a) The starting phase to generate the first half of the border,
+ // b) The middle phase, where a single vertex reference gets created, and
+ // c) The ending phase to complete the generation of the border.
+ // 4) Each border is generated in two variants:
+ // a) one variant that mates with a tile at the same level of detail, and
+ // b) another variant that mates with a tile at the next lower level of detail.
+ // 5) Borders that mate with the next lower level of detail are constrained to lie on even indices.
+ // 6) Evenness is generated by ANDing the index with a mask that has 1's in all bits except for the LSB,
+ // which results in clearing the LSB os the index, making it even.
+ // 7) The section that generates lower level LOD borders gives up any attempt to be optimal because of the
+ // complexity. Instead, correctness was preferred. That said, any performance lost is in the noise,
+ // since this code only gets run once.
+
+ /*
+ * The following section of code generates full resolution boundary meshes. These are used to mate
+ * with neighboring tiles that are at the same level of detail.
+ */
+ // North border.
+ numIndices = 2 * numLonVertices - 2;
+ latIndex = numLatVertices - 1;
+
+ // Corner vertex.
+ lonIndex = numLonVertices - 1;
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+
+ for (lonIndex = numLonVertices - 2; lonIndex > 0; lonIndex -= 1) {
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+ indices[index++] = vertexIndex - numLonVertices;
+ }
+
+ // Corner vertex.
+ lonIndex = 0;
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+
+ this.indicesNorthOffset = indices.length - numIndices;
+ this.indicesNorth = new Uint16Array(indices.slice(this.indicesNorthOffset));
+ this.numIndicesNorth = numIndices;
+
+ // South border.
+ numIndices = 2 * numLonVertices - 2;
+ latIndex = 0;
+
+ // Corner vertex.
+ lonIndex = 0;
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+
+ for (lonIndex = 1; lonIndex < numLonVertices - 1; lonIndex += 1) {
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+ indices[index++] = vertexIndex + numLonVertices;
+ }
+
+ // Corner vertex.
+ lonIndex = numLonVertices - 1;
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+
+ this.indicesSouthOffset = indices.length - numIndices;
+ this.indicesSouth = new Uint16Array(indices.slice(this.indicesSouthOffset));
+ this.numIndicesSouth = numIndices;
+
+ // West border.
+ numIndices = 2 * numLatVertices - 2;
+ lonIndex = 0;
+
+ // Corner vertex.
+ latIndex = numLatVertices - 1;
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+
+ for (latIndex = numLatVertices - 2; latIndex > 0; latIndex -= 1) {
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+ indices[index++] = vertexIndex + 1;
+ }
+
+ // Corner vertex.
+ latIndex = 0;
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+
+ this.indicesWestOffset = indices.length - numIndices;
+ this.indicesWest = new Uint16Array(indices.slice(this.indicesWestOffset));
+ this.numIndicesWest = numIndices;
+
+ // East border.
+ numIndices = 2 * numLatVertices - 2;
+ lonIndex = numLonVertices - 1;
+
+ // Corner vertex.
+ latIndex = 0;
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+
+ for (latIndex = 1; latIndex < numLatVertices - 1; latIndex += 1) {
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+ indices[index++] = vertexIndex - 1;
+ }
+
+ // Corner vertex.
+ latIndex = numLatVertices - 1;
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+
+ this.indicesEastOffset = indices.length - numIndices;
+ this.indicesEast = new Uint16Array(indices.slice(this.indicesEastOffset));
+ this.numIndicesEast = numIndices;
+
+ /*
+ * The following section of code generates "lores" low resolution boundary meshes. These are used to mate
+ * with neighboring tiles that are at a lower level of detail. The property of these lower level meshes is that
+ * they have half the number of vertices.
+ *
+ * To generate the boundary meshes, force the use of only even boundary vertex indices.
+ */
+ // North border.
+ numIndices = 2 * numLonVertices - 2;
+ latIndex = numLatVertices - 1;
+
+ // Corner vertex.
+ lonIndex = numLonVertices - 1;
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+
+ for (lonIndex = numLonVertices - 2; lonIndex > 0; lonIndex -= 1) {
+ // Exterior vertex rounded up to even index.
+ vertexIndex = (lonIndex + 1 & ~1) + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+
+ // Interior vertex.
+ vertexIndex = lonIndex + (latIndex - 1) * numLonVertices;
+ indices[index++] = vertexIndex;
+ }
+
+ // Corner vertex.
+ lonIndex = 0;
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+
+ this.indicesLoresNorthOffset = indices.length - numIndices;
+ this.indicesLoresNorth = new Uint16Array(indices.slice(this.indicesLoresNorthOffset));
+ this.numIndicesLoresNorth = numIndices;
+
+ // South border.
+ numIndices = 2 * numLonVertices - 2;
+ latIndex = 0;
+
+ // Corner vertex.
+ lonIndex = 0;
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+
+ for (lonIndex = 1; lonIndex < numLonVertices - 1; lonIndex += 1) {
+ // Exterior Vertex rounded down to even index.
+ vertexIndex = (lonIndex & ~1) + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+
+ // Interior vertex.
+ vertexIndex = lonIndex + (latIndex + 1) * numLonVertices;
+ indices[index++] = vertexIndex;
+ }
+
+ // Corner vertex.
+ lonIndex = numLonVertices - 1;
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+
+ this.indicesLoresSouthOffset = indices.length - numIndices;
+ this.indicesLoresSouth = new Uint16Array(indices.slice(this.indicesLoresSouthOffset));
+ this.numIndicesLoresSouth = numIndices;
+
+ // West border.
+ numIndices = 2 * numLatVertices - 2;
+ lonIndex = 0;
+
+ // Corner vertex.
+ latIndex = numLatVertices - 1;
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+
+ for (latIndex = numLatVertices - 2; latIndex > 0; latIndex -= 1) {
+ // Exterior Vertex rounded up to even index.
+ vertexIndex = lonIndex + (latIndex + 1 & ~1) * numLonVertices;
+ indices[index++] = vertexIndex;
+
+ // Interior vertex.
+ vertexIndex = lonIndex + 1 + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+ }
+
+ // Corner vertex.
+ latIndex = 0;
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+
+ this.indicesLoresWestOffset = indices.length - numIndices;
+ this.indicesLoresWest = new Uint16Array(indices.slice(this.indicesLoresWestOffset));
+ this.numIndicesLoresWest = numIndices;
+
+ // East border.
+ numIndices = 2 * numLatVertices - 2;
+ lonIndex = numLonVertices - 1;
+
+ // Corner vertex.
+ latIndex = 0;
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+
+ for (latIndex = 1; latIndex < numLatVertices - 1; latIndex += 1) {
+ // Exterior vertex rounded down to even index.
+ vertexIndex = lonIndex + (latIndex & ~1) * numLonVertices;
+ indices[index++] = vertexIndex;
+
+ // Interior vertex.
+ vertexIndex = lonIndex - 1 + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+ }
+
+ // Corner vertex.
+ latIndex = numLatVertices - 1;
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index++] = vertexIndex;
+
+ this.indicesLoresEastOffset = indices.length - numIndices;
+ this.indicesLoresEast = new Uint16Array(indices.slice(this.indicesLoresEastOffset));
+ this.numIndicesLoresEast = numIndices;
+
+ var wireframeIndices = this.buildWireframeIndices(tileWidth, tileHeight);
+ var outlineIndices = this.buildOutlineIndices(tileWidth, tileHeight);
+
+ indices = indices.concat(wireframeIndices);
+ this.wireframeIndicesOffset = indices.length - this.numWireframeIndices;
+
+ indices = indices.concat(outlineIndices);
+ this.outlineIndicesOffset = indices.length - this.numOutlineIndices;
+
+ this.indices = new Uint16Array(indices);
+};
+
+Tessellator.prototype.buildWireframeIndices = function (tileWidth, tileHeight) {
+ // The wireframe representation draws the vertices that appear on the surface.
+
+ // The number of vertices in each dimension is 1 more than the number of cells.
+ var numLatVertices = tileHeight + 1;
+ var numLonVertices = tileWidth + 1;
+
+ // Allocate an array to hold the computed indices.
+ var numIndices = 2 * tileWidth * numLatVertices + 2 * tileHeight * numLonVertices;
+ var indices = [];
+
+ var rowStride = numLonVertices;
+
+ var index = 0,
+ lonIndex,
+ latIndex,
+ vertexIndex;
+
+ // Add a line between each row to define the horizontal cell outlines.
+ for (latIndex = 0; latIndex < numLatVertices; latIndex += 1) {
+ for (lonIndex = 0; lonIndex < tileWidth; lonIndex += 1) {
+ vertexIndex = lonIndex + latIndex * rowStride;
+ indices[index] = vertexIndex;
+ indices[index + 1] = vertexIndex + 1;
+ index += 2;
+ }
+ }
+
+ // Add a line between each column to define the vertical cell outlines.
+ for (lonIndex = 0; lonIndex < numLonVertices; lonIndex += 1) {
+ for (latIndex = 0; latIndex < tileHeight; latIndex += 1) {
+ vertexIndex = lonIndex + latIndex * rowStride;
+ indices[index] = vertexIndex;
+ indices[index + 1] = vertexIndex + rowStride;
+ index += 2;
+ }
+ }
+
+ this.numWireframeIndices = numIndices;
+ return indices;
+};
+
+Tessellator.prototype.buildOutlineIndices = function (tileWidth, tileHeight) {
+ // The outline representation traces the tile's outer edge on the surface.
+
+ // The number of vertices in each dimension is 1 more than the number of cells.
+ var numLatVertices = tileHeight + 1;
+ var numLonVertices = tileWidth + 1;
+
+ // Allocate an array to hold the computed indices.
+ var numIndices = 2 * (numLatVertices - 2) + 2 * numLonVertices + 1;
+ var indices = [];
+
+ var rowStride = numLatVertices;
+
+ var index = 0,
+ lonIndex,
+ latIndex,
+ vertexIndex;
+
+ // Bottom row, starting at the left and going right.
+ latIndex = 0;
+ for (lonIndex = 0; lonIndex < numLonVertices; lonIndex += 1) {
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index] = vertexIndex;
+ index += 1;
+ }
+
+ // Right column, starting at the bottom and going up.
+ lonIndex = numLonVertices - 1;
+ for (latIndex = 1; latIndex < numLatVertices; latIndex += 1) {
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index] = vertexIndex;
+ index += 1;
+ }
+
+ // Top row, starting on the right and going to the left.
+ latIndex = numLatVertices - 1;
+ for (lonIndex = numLonVertices - 1; lonIndex >= 0; lonIndex -= 1) {
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index] = vertexIndex;
+ index += 1;
+ }
+
+ // Leftmost column, starting at the top and going down.
+ lonIndex = 0;
+ for (latIndex = numLatVertices - 1; latIndex >= 0; latIndex -= 1) {
+ vertexIndex = lonIndex + latIndex * numLonVertices;
+ indices[index] = vertexIndex;
+ index += 1;
+ }
+
+ this.numOutlineIndices = numIndices;
+ return indices;
+};
+
+Tessellator.prototype.cacheSharedGeometryVBOs = function (dc) {
+ var gl = dc.currentGlContext,
+ gpuResourceCache = dc.gpuResourceCache;
+
+ var texCoordVbo = gpuResourceCache.resourceForKey(this.texCoordVboCacheKey);
+ if (!texCoordVbo) {
+ texCoordVbo = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, texCoordVbo);
+ gl.bufferData(gl.ARRAY_BUFFER, this.texCoords, gl.STATIC_DRAW);
+ dc.frameStatistics.incrementVboLoadCount(1);
+ gpuResourceCache.putResource(this.texCoordVboCacheKey, texCoordVbo, this.texCoords.length * 4 / 2);
+ }
+
+ var indicesVbo = gpuResourceCache.resourceForKey(this.indicesVboCacheKey);
+ if (!indicesVbo) {
+ indicesVbo = gl.createBuffer();
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indicesVbo);
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, this.indices, gl.STATIC_DRAW);
+ dc.frameStatistics.incrementVboLoadCount(1);
+ gpuResourceCache.putResource(this.indicesVboCacheKey, indicesVbo, this.indices.length * 2);
+ }
+};
+
+export default Tessellator;
diff --git a/web/test/WebWorldWind/src/globe/TiledElevationCoverage.js b/web/test/WebWorldWind/src/globe/TiledElevationCoverage.js
new file mode 100644
index 00000000..4c92158f
--- /dev/null
+++ b/web/test/WebWorldWind/src/globe/TiledElevationCoverage.js
@@ -0,0 +1,635 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports TiledElevationCoverage
+ */
+import AbsentResourceList from '../util/AbsentResourceList';
+import Angle from '../geom/Angle';
+import ArgumentError from '../error/ArgumentError';
+import ElevationCoverage from '../globe/ElevationCoverage';
+import ElevationImage from '../globe/ElevationImage';
+import LevelSet from '../util/LevelSet';
+import Location from '../geom/Location';
+import Logger from '../util/Logger';
+import MemoryCache from '../cache/MemoryCache';
+import Sector from '../geom/Sector';
+import Tile from '../util/Tile';
+import WWMath from '../util/WWMath';
+
+/**
+ * Constructs a TiledElevationCoverage
+ * @alias TiledElevationCoverage
+ * @constructor
+ * @classdesc Represents the elevations for an area, often but not necessarily the whole globe.
+ * @param {{}} config Configuration properties for the coverage:
+ *
+ * While the image tiles for this class are typically drawn from a remote server such as a WMS server. The actual + * retrieval protocol is independent of this class and encapsulated by a class implementing the {@link UrlBuilder} + * interface and associated with instances of this class as a property. + *
+ * There is no requirement that image tiles of this class be remote, they may be local or procedurally generated. For + * such cases the subclass overrides this class' [retrieveTileImage]{@link TiledImageLayer#retrieveTileImage} method. + *
+ * Layers of this type are by default not pickable. Their pick-enabled flag is initialized to false. + * + * @augments Layer + * @param {Sector} sector The sector this layer covers. + * @param {Location} levelZeroDelta The size in latitude and longitude of level zero (lowest resolution) tiles. + * @param {Number} numLevels The number of levels to define for the layer. Each level is successively one power + * of two higher resolution than the next lower-numbered level. (0 is the lowest resolution level, 1 is twice + * that resolution, etc.) + * Each level contains four times as many tiles as the next lower-numbered level, each 1/4 the geographic size. + * @param {String} imageFormat The mime type of the image format for the layer's tiles, e.g., image/png. + * @param {String} cachePath A string uniquely identifying this layer relative to other layers. + * @param {Number} tileWidth The horizontal size of image tiles in pixels. + * @param {Number} tileHeight The vertical size of image tiles in pixels. + * @throws {ArgumentError} If any of the specified sector, level-zero delta, cache path or image format arguments are + * null or undefined, or if the specified number of levels, tile width or tile height is less than 1. + * + */ +function TiledImageLayer(sector, levelZeroDelta, numLevels, imageFormat, cachePath, tileWidth, tileHeight) { + if (!sector) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "TiledImageLayer", "constructor", "missingSector")); + } + + if (!levelZeroDelta) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "TiledImageLayer", "constructor", + "The specified level-zero delta is null or undefined.")); + } + + if (!imageFormat) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "TiledImageLayer", "constructor", + "The specified image format is null or undefined.")); + } + + if (!cachePath) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "TiledImageLayer", "constructor", + "The specified cache path is null or undefined.")); + } + + if (!numLevels || numLevels < 1) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "TiledImageLayer", "constructor", + "The specified number of levels is less than one.")); + } + + if (!tileWidth || !tileHeight || tileWidth < 1 || tileHeight < 1) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "TiledImageLayer", "constructor", + "The specified tile width or height is less than one.")); + } + + Layer.call(this, "Tiled Image Layer"); + + this.retrievalImageFormat = imageFormat; + this.cachePath = cachePath; + + /** + * Controls how many concurrent tile requests are allowed for this layer. + * @type {Number} + * @default WorldWind.configuration.layerRetrievalQueueSize + */ + this.retrievalQueueSize = WorldWind.configuration.layerRetrievalQueueSize; + + this.levels = new LevelSet(sector, levelZeroDelta, numLevels, tileWidth, tileHeight); + + /** + * Controls the level of detail switching for this layer. The next highest resolution level is + * used when an image's texel size is greater than this number of pixels, up to the maximum resolution + * of this layer. + * @type {Number} + * @default 1.75 + */ + this.detailControl = 3.5; + + /** + * Indicates whether credentials are sent when requesting images from a different origin. + * + * Allowed values are anonymous and use-credentials. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-crossorigin + * @type {string} + * @default anonymous + */ + this.crossOrigin = 'anonymous'; + + /* Intentionally not documented. + * Indicates the time at which this layer's imagery expire. Expired images are re-retrieved + * when the current time exceeds the specified expiry time. If null, images do not expire. + * @type {Date} + */ + this.expiration = null; + + this.currentTiles = []; + this.currentTilesInvalid = true; + this.tileCache = new MemoryCache(500000, 400000); + this.currentRetrievals = []; + this.absentResourceList = new AbsentResourceList(3, 50e3); + + this.pickEnabled = false; + + // Internal. Intentionally not documented. + this.lasTtMVP = Matrix.fromIdentity(); +} + +TiledImageLayer.prototype = Object.create(Layer.prototype); + +// Inherited from Layer. +TiledImageLayer.prototype.refresh = function () { + this.expiration = new Date(); + this.currentTilesInvalid = true; +}; + +/** + * Initiates retrieval of this layer's level 0 images. Use + * [isPrePopulated]{@link TiledImageLayer#isPrePopulated} to determine when the images have been retrieved + * and associated with the level 0 tiles. + * Pre-populating is not required. It is used to eliminate the visual effect of loading tiles incrementally, + * but only for level 0 tiles. An application might pre-populate a layer in order to delay displaying it + * within a time series until all the level 0 images have been retrieved and added to memory. + * @param {WorldWindow} wwd The WorldWindow for which to pre-populate this layer. + * @throws {ArgumentError} If the specified WorldWindow is null or undefined. + */ +TiledImageLayer.prototype.prePopulate = function (wwd) { + if (!wwd) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "TiledImageLayer", "prePopulate", "missingWorldWindow")); + } + + var dc = wwd.drawContext; + + if (!this.topLevelTiles || this.topLevelTiles.length === 0) { + this.createTopLevelTiles(dc); + } + + for (var i = 0; i < this.topLevelTiles.length; i++) { + var tile = this.topLevelTiles[i]; + + if (!this.isTileTextureInMemory(dc, tile)) { + this.retrieveTileImage(dc, tile, true); // suppress redraw upon successful retrieval + } + } +}; + +/** + * Initiates retrieval of this layer's tiles that are visible in the specified WorldWindow. Pre-populating is + * not required. It is used to eliminate the visual effect of loading tiles incrementally. + * @param {WorldWindow} wwd The WorldWindow for which to pre-populate this layer. + * @throws {ArgumentError} If the specified WorldWindow is null or undefined. + */ +TiledImageLayer.prototype.prePopulateCurrentTiles = function (wwd) { + if (!wwd) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "TiledImageLayer", "prePopulate", "missingWorldWindow")); + } + + var dc = wwd.drawContext; + this.assembleTiles(dc); + + for (var i = 0, len = this.currentTiles.length; i < len; i++) { + var tile = this.currentTiles[i]; + + if (!this.isTileTextureInMemory(dc, tile)) { + this.retrieveTileImage(dc, tile, true); // suppress redraw upon successful retrieval + } + } +}; + +/** + * Indicates whether this layer's level 0 tile images have been retrieved and associated with the tiles. + * Use [prePopulate]{@link TiledImageLayer#prePopulate} to initiate retrieval of level 0 images. + * @param {WorldWindow} wwd The WorldWindow associated with this layer. + * @returns {Boolean} true if all level 0 images have been retrieved, otherwise false. + * @throws {ArgumentError} If the specified WorldWindow is null or undefined. + */ +TiledImageLayer.prototype.isPrePopulated = function (wwd) { + if (!wwd) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "TiledImageLayer", "isPrePopulated", "missingWorldWindow")); + } + + for (var i = 0; i < this.topLevelTiles.length; i++) { + if (!this.isTileTextureInMemory(wwd.drawContext, this.topLevelTiles[i])) { + return false; + } + } + + return true; +}; + +// Intentionally not documented. +TiledImageLayer.prototype.createTile = function (sector, level, row, column) { + var path = this.cachePath + "-layer/" + level.levelNumber + "/" + row + "/" + column + "." + + WWUtil.suffixForMimeType(this.retrievalImageFormat); + + return new ImageTile(sector, level, row, column, path); +}; + +// Documented in superclass. +TiledImageLayer.prototype.doRender = function (dc) { + if (!dc.terrain) + return; + + if (this.currentTilesInvalid + || !dc.modelviewProjection.equals(this.lasTtMVP) + || dc.globeStateKey !== this.lastGlobeStateKey) { + this.currentTilesInvalid = false; + + // Tile fading works visually only when the surface tiles are opaque, otherwise the surface flashes + // when two tiles are drawn over the same area, even though one of them is semi-transparent. + // So do not provide fading when the surface opacity is less than 1; + if (dc.surfaceOpacity >= 1 && this.opacity >= 1) { + // Fading of outgoing tiles requires determination of the those tiles. Prepare an object with all of + // the preceding frame's tiles so that we can subsequently compare the list of newly selected tiles + // with the previously selected tiles. + this.previousTiles = {}; + for (var j = 0; j < this.currentTiles.length; j++) { + this.previousTiles[this.currentTiles[j].imagePath] = this.currentTiles[j]; + } + + this.assembleTiles(dc); + this.fadeOutgoingTiles(dc); + } else { + this.assembleTiles(dc); + } + + } + + this.lasTtMVP.copy(dc.modelviewProjection); + this.lastGlobeStateKey = dc.globeStateKey; + + if (this.currentTiles.length > 0) { + dc.surfaceTileRenderer.renderTiles(dc, this.currentTiles, this.opacity, dc.surfaceOpacity >= 1); + dc.frameStatistics.incrementImageTileCount(this.currentTiles.length); + this.inCurrentFrame = true; + } +}; + +TiledImageLayer.prototype.fadeOutgoingTiles = function (dc) { + // Determine which files are outgoing and fade their disappearance. Must be called after this frame's + // current tiles for this layer have been determined. + + var visibilityDelta = (dc.timestamp - dc.previousRedrawTimestamp) / dc.fadeTime; + + // Create a hash table of the current tiles so that we can check for tile inclusion below. + var current = {}; + for (var i = 0; i < this.currentTiles.length; i++) { + var tile = this.currentTiles[i]; + current[tile.imagePath] = tile; + } + + // Determine whether the tile was in the previous frame but is not in this one. If that's the case, + // then the tile is outgoing and its opacity needs to be reduced. + for (var tileImagePath in this.previousTiles) { + if (this.previousTiles.hasOwnProperty(tileImagePath)) { + tile = this.previousTiles[tileImagePath]; + + if (tile.opacity > 0 && !current[tile.imagePath]) { + // Compute the reduced. + tile.opacity = Math.max(0, tile.opacity - visibilityDelta); + + // If not fully faded, add the tile to the list of current tiles and request a redraw so that + // we'll be called continuously until all tiles have faded completely. Note that order in the + // current tiles list is important: the non-opaque tiles must be drawn after the opaque tiles. + if (tile.opacity > 0) { + this.currentTiles.push(tile); + this.currentTilesInvalid = true; + dc.redrawRequested = true; + } + } + } + } +}; + +// Documented in superclass. +TiledImageLayer.prototype.isLayerInView = function (dc) { + return dc.terrain && dc.terrain.sector && dc.terrain.sector.intersects(this.levels.sector); +}; + +// Documented in superclass. +TiledImageLayer.prototype.createTopLevelTiles = function (dc) { + this.topLevelTiles = []; + Tile.createTilesForLevel(this.levels.firstLevel(), this, this.topLevelTiles); +}; + +// Intentionally not documented. +TiledImageLayer.prototype.assembleTiles = function (dc) { + this.currentTiles = []; + + if (!this.topLevelTiles || this.topLevelTiles.length === 0) { + this.createTopLevelTiles(dc); + } + + for (var i = 0, len = this.topLevelTiles.length; i < len; i++) { + var tile = this.topLevelTiles[i]; + + tile.update(dc); + + this.currentAncestorTile = null; + + if (this.isTileVisible(dc, tile)) { + this.addTileOrDescendants(dc, tile); + } + } +}; + +// Intentionally not documented. +TiledImageLayer.prototype.addTileOrDescendants = function (dc, tile) { + if (this.tileMeetsRenderingCriteria(dc, tile)) { + this.addTile(dc, tile); + return; + } + + var ancestorTile = null; + + try { + if (this.isTileTextureInMemory(dc, tile) || tile.level.levelNumber === 0) { + ancestorTile = this.currentAncestorTile; + this.currentAncestorTile = tile; + } + + var nextLevel = this.levels.level(tile.level.levelNumber + 1), + subTiles = tile.subdivideToCache(nextLevel, this, this.tileCache); + + for (var i = 0, len = subTiles.length; i < len; i++) { + var child = subTiles[i]; + + child.update(dc); + + if (this.levels.sector.intersects(child.sector) && this.isTileVisible(dc, child)) { + this.addTileOrDescendants(dc, child); + } + } + } finally { + if (ancestorTile) { + this.currentAncestorTile = ancestorTile; + } + } +}; + +// Intentionally not documented. +TiledImageLayer.prototype.addTile = function (dc, tile) { + tile.fallbackTile = null; + + var texture = dc.gpuResourceCache.resourceForKey(tile.imagePath); + if (texture) { + tile.opacity = 1; + + this.currentTiles.push(tile); + + // If the tile's texture has expired, cause it to be re-retrieved. Note that the current, + // expired texture is still used until the updated one arrives. + if (this.expiration && this.isTextureExpired(texture)) { + this.retrieveTileImage(dc, tile); + } + + return; + } + + this.retrieveTileImage(dc, tile); + + if (this.currentAncestorTile) { + if (this.isTileTextureInMemory(dc, this.currentAncestorTile)) { + // Set up to map the ancestor tile into the current one. + tile.fallbackTile = this.currentAncestorTile; + tile.fallbackTile.opacity = 1; + this.currentTiles.push(tile); + } + } +}; + +// Intentionally not documented. +TiledImageLayer.prototype.isTileVisible = function (dc, tile) { + if (dc.globe.projectionLimits && !tile.sector.overlaps(dc.globe.projectionLimits)) { + return false; + } + + return tile.extent.intersectsFrustum(dc.frustumInModelCoordinates); +}; + +// Intentionally not documented. +TiledImageLayer.prototype.tileMeetsRenderingCriteria = function (dc, tile) { + var s = this.detailControl; + var highLatitude = WWMath.mercatorLat(75); + if (tile.sector.minLatitude >= highLatitude || tile.sector.maxLatitude <= -highLatitude) { + s *= 1.2; + } + return tile.level.isLastLevel() || !tile.mustSubdivide(dc, s); +}; + +// Intentionally not documented. +TiledImageLayer.prototype.isTileTextureInMemory = function (dc, tile) { + return dc.gpuResourceCache.containsResource(tile.imagePath); +}; + +// Intentionally not documented. +TiledImageLayer.prototype.isTextureExpired = function (texture) { + return this.expiration && texture.creationTime.getTime() <= this.expiration.getTime(); +}; + +/** + * Retrieves the image for the specified tile. Subclasses should override this method in order to retrieve, + * compute or otherwise create the image. + * @param {DrawContext} dc The current draw context. + * @param {ImageTile} tile The tile for which to retrieve the resource. + * @param {Boolean} suppressRedraw true to suppress generation of redraw events when an image is successfully + * retrieved, otherwise false. + * @protected + */ +TiledImageLayer.prototype.retrieveTileImage = function (dc, tile, suppressRedraw) { + if (this.currentRetrievals.indexOf(tile.imagePath) < 0) { + if (this.currentRetrievals.length > this.retrievalQueueSize) { + return; + } + + if (this.absentResourceList.isResourceAbsent(tile.imagePath)) { + return; + } + + var url = this.resourceUrlForTile(tile, this.retrievalImageFormat), + image = new Image(), + imagePath = tile.imagePath, + cache = dc.gpuResourceCache, + canvas = dc.currentGlContext.canvas, + layer = this; + + if (!url) { + this.currentTilesInvalid = true; + return; + } + + image.onload = function () { + Logger.log(Logger.LEVEL_INFO, "Image retrieval succeeded: " + url); + var texture = layer.createTexture(dc, tile, image); + layer.removeFromCurrentRetrievals(imagePath); + + if (texture) { + cache.putResource(imagePath, texture, texture.size); + + layer.currentTilesInvalid = true; + layer.absentResourceList.unmarkResourceAbsent(imagePath); + + if (!suppressRedraw) { + // Send an event to request a redraw. + var e = document.createEvent('Event'); + e.initEvent(WorldWind.REDRAW_EVENT_TYPE, true, true); + canvas.dispatchEvent(e); + } + } + }; + + image.onerror = function () { + layer.removeFromCurrentRetrievals(imagePath); + layer.absentResourceList.markResourceAbsent(imagePath); + Logger.log(Logger.LEVEL_WARNING, "Image retrieval failed: " + url); + }; + + this.currentRetrievals.push(imagePath); + image.crossOrigin = this.crossOrigin; + image.src = url; + } +}; + +// Intentionally not documented. +TiledImageLayer.prototype.createTexture = function (dc, tile, image) { + return new Texture(dc.currentGlContext, image); +}; + +// Intentionally not documented. +TiledImageLayer.prototype.removeFromCurrentRetrievals = function (imagePath) { + var index = this.currentRetrievals.indexOf(imagePath); + if (index > -1) { + this.currentRetrievals.splice(index, 1); + } +}; + +/** + * Returns the URL string for the resource. + * @param {ImageTile} tile The tile whose image is returned + * @param {String} imageFormat The mime type of the image format desired. + * @returns {String} The URL string, or null if the string can not be formed. + * @protected + */ +TiledImageLayer.prototype.resourceUrlForTile = function (tile, imageFormat) { + if (this.urlBuilder) { + return this.urlBuilder.urlForTile(tile, imageFormat); + } else { + return null; + } +}; + +export default TiledImageLayer; diff --git a/web/test/WebWorldWind/src/layer/XYZLayer.js b/web/test/WebWorldWind/src/layer/XYZLayer.js new file mode 100644 index 00000000..4dbba1cc --- /dev/null +++ b/web/test/WebWorldWind/src/layer/XYZLayer.js @@ -0,0 +1,30 @@ +import MercatorTiledImageLayer from './MercatorTiledImageLayer'; +import Sector from '../geom/Sector'; +import Location from '../geom/Location'; + +class URLBuilder { + urlForTile(tile, imageFormat) { + return `http://localhost:2020/api/Map/Tiles?x=${tile.column}&y=${tile.row}&z=${tile.level.levelNumber}`; + } +} + +class XYZLayer extends MercatorTiledImageLayer { + constructor() { + let imageSize = 256; + let displayName = 'Bing'; + super(new Sector(-180, 180, -180, 180), new Location(360, 360), 18, "image/jpeg", + displayName, imageSize, imageSize); + + this.imageSize = imageSize; + this.displayName = displayName; + this.urlBuilder = new URLBuilder(); + } + + createTopLevelTiles(dc) { + this.topLevelTiles = []; + + this.topLevelTiles.push(this.createTile(null, this.levels.firstLevel(), 0, 0)); + } +} + +export default XYZLayer; \ No newline at end of file diff --git a/web/test/WebWorldWind/src/navigate/LookAtNavigator.js b/web/test/WebWorldWind/src/navigate/LookAtNavigator.js new file mode 100644 index 00000000..192c66e6 --- /dev/null +++ b/web/test/WebWorldWind/src/navigate/LookAtNavigator.js @@ -0,0 +1,54 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports LookAtNavigator + */ +import Location from '../geom/Location'; +import Navigator from '../navigate/Navigator'; + + +/** + * Constructs a look-at navigator. + * @alias LookAtNavigator + * @constructor + * @augments Navigator + * @classdesc Represents a navigator containing the required variables to enable the user to pan, zoom and tilt + * the globe. + */ +function LookAtNavigator() { + Navigator.call(this); + + /** + * The geographic location at the center of the viewport. + * @type {Location} + */ + this.lookAtLocation = new Location(36.4, 117); + + /** + * The distance from this navigator's eye point to its look-at location. + * @type {Number} + * @default 10,000 kilometers + */ + this.range = 10e6; // TODO: Compute initial range to fit globe in viewport. + + // Development testing only. Set this to false to suppress default navigator limits on 2D globes. + this.enable2DLimits = true; +} + +LookAtNavigator.prototype = Object.create(Navigator.prototype); + +export default LookAtNavigator; diff --git a/web/test/WebWorldWind/src/navigate/Navigator.js b/web/test/WebWorldWind/src/navigate/Navigator.js new file mode 100644 index 00000000..3b8a9eab --- /dev/null +++ b/web/test/WebWorldWind/src/navigate/Navigator.js @@ -0,0 +1,53 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports Navigator + */ + + + +/** + * Constructs a base navigator. + * @alias Navigator + * @constructor + * @classdesc Provides an abstract base class for navigators. This class is not meant to be instantiated + * directly. See {@Link LookAtNavigator} for a concrete navigator. + */ +function Navigator() { + /** + * This navigator's heading, in degrees clockwise from north. + * @type {Number} + * @default 0 + */ + this.heading = 0; + + /** + * This navigator's tilt, in degrees. + * @type {Number} + * @default 0 + */ + this.tilt = 0; + + /** + * This navigator's roll, in degrees. + * @type {Number} + * @default 0 + */ + this.roll = 0; +} + +export default Navigator; diff --git a/web/test/WebWorldWind/src/pick/PickedObject.js b/web/test/WebWorldWind/src/pick/PickedObject.js new file mode 100644 index 00000000..83f4a8c3 --- /dev/null +++ b/web/test/WebWorldWind/src/pick/PickedObject.js @@ -0,0 +1,78 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports PickedObject + */ + + + +/** + * Constructs a picked object. + * @alias PickedObject + * @constructor + * @classdesc Represents a picked object. + * @param {Color} color The pick color identifying the object. + * @param {Object} userObject An object to associate with this picked object, usually the picked shape. + * @param {Position} position The picked object's geographic position. May be null if unknown. + * @param {Layer} parentLayer The layer containing the picked object. + * @param {Boolean} isTerrain true if the picked object is terrain, otherwise false. + */ +function PickedObject(color, userObject, position, parentLayer, isTerrain) { + + /** + * This picked object's pick color. + * @type {Color} + * @readonly + */ + this.color = color; + + /** + * The picked shape. + * @type {Object} + * @readonly + */ + this.userObject = userObject; + + /** + * This picked object's geographic position. + * @type {Position} + * @readonly + */ + this.position = position; + + /** + * The layer containing this picked object. + * @type {Layer} + * @readonly + */ + this.parentLayer = parentLayer; + + /** + * Indicates whether this picked object is terrain. + * @type {Boolean} + * @readonly + */ + this.isTerrain = isTerrain; + + /** + * Indicates whether this picked object is the top object. + * @type {boolean} + */ + this.isOnTop = false; +} + +export default PickedObject; diff --git a/web/test/WebWorldWind/src/pick/PickedObjectList.js b/web/test/WebWorldWind/src/pick/PickedObjectList.js new file mode 100644 index 00000000..0cbcc7da --- /dev/null +++ b/web/test/WebWorldWind/src/pick/PickedObjectList.js @@ -0,0 +1,114 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports PickedObjectList + */ + + + +/** + * Constructs a picked-object list. + * @alias PickedObjectList + * @constructor + * @classdesc Holds a collection of picked objects. + */ +function PickedObjectList() { + /** + * The picked objects. + * @type {Array} + */ + this.objects = []; +} + +/** + * Indicates whether this list contains picked objects that are not terrain. + * @returns {Boolean} true if this list contains objects that are not terrain, + * otherwise false. + */ +PickedObjectList.prototype.hasNonTerrainObjects = function () { + return this.objects.length > 1 || this.objects.length === 1 && this.terrainObject() == null; +}; + +/** + * Returns the terrain object within this list, if this list contains a terrain object. + * @returns {PickedObject} The terrain object, or null if this list does not contain a terrain object. + */ +PickedObjectList.prototype.terrainObject = function () { + for (var i = 0, len = this.objects.length; i < len; i++) { + if (this.objects[i].isTerrain) { + return this.objects[i]; + } + } + + return null; +}; + +/** + * Adds a picked object to this list. + * If the picked object is a terrain object and the list already contains a terrain object, the terrain + * object in the list is replaced by the specified one. + * @param {PickedObject} pickedObject The picked object to add. If null, this list remains unchanged. + */ +PickedObjectList.prototype.add = function (pickedObject) { + if (pickedObject) { + if (pickedObject.isTerrain) { + var terrainObjectIndex = this.objects.length; + + for (var i = 0, len = this.objects.length; i < len; i++) { + if (this.objects[i].isTerrain) { + terrainObjectIndex = i; + break; + } + } + + this.objects[terrainObjectIndex] = pickedObject; + } else { + this.objects.push(pickedObject); + } + } +}; + +/** + * Removes all items from this list. + */ +PickedObjectList.prototype.clear = function () { + this.objects = []; +}; + +/** + * Returns the top-most picked object in this list. + * @returns {PickedObject} The top-most picked object in this list, or null if this list is empty. + */ +PickedObjectList.prototype.topPickedObject = function () { + var size = this.objects.length; + + if (size > 1) { + for (var i = 0; i < size; i++) { + if (this.objects[i].isOnTop) { + return this.objects[i]; + } + } + } + + if (size > 0) { + return this.objects[0]; + } + + return null; +}; + +export default PickedObjectList; diff --git a/web/test/WebWorldWind/src/projections/GeographicProjection.js b/web/test/WebWorldWind/src/projections/GeographicProjection.js new file mode 100644 index 00000000..760c80ca --- /dev/null +++ b/web/test/WebWorldWind/src/projections/GeographicProjection.js @@ -0,0 +1,252 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports GeographicProjection + */ +import ArgumentError from '../error/ArgumentError'; +import Logger from '../util/Logger'; +import UnsupportedOperationError from '../error/UnsupportedOperationError'; + + +/** + * Constructs a base geographic projection. + * @alias GeographicProjection + * @constructor + * @classdesc Represents a geographic projection. + * This is an abstract class and is meant to be instantiated only by subclasses. + * See the following projections: + *
+ * This method is used to compute a collection of points within a sector. It is used by tessellators to + * efficiently generate a tile's interior points. The number of points to generate is indicated by the tileWidth + * and tileHeight parameters but is one more in each direction. Width refers to the longitudinal direction, + * height to the latitudinal. + *
+ * For each implied position within the sector, an elevation value is specified via an array of elevations. The
+ * calculation at each position incorporates the associated elevation.
+ * There must be (tileWidth + 1) x (tileHeight + 1) elevations in the array.
+ *
+ * @param {Globe} globe The globe this projection applies to.
+ * @param {Sector} sector The sector in which to compute the points.
+ * @param {Number} numLat The number of latitudinal sections a tile is divided into.
+ * @param {Number} numLon The number of longitudinal sections a tile is divided into.
+ * @param {Number[]} elevations An array of elevations to incorporate in the point calculations. There must be
+ * one elevation value in the array for each generated point. Elevations are in meters.
+ * There must be (tileWidth + 1) x (tileHeight + 1) elevations in the array.
+ * @param {Vec3} referencePoint The X, Y and Z Cartesian coordinates to subtract from the computed coordinates.
+ * This makes the computed coordinates relative to the specified point. May be null.
+ * @param {Vec3} offset An offset to apply to the Cartesian output points. Typically only projections that
+ * are continuous (see [continuous]{@link GeographicProjection#continuous}) apply this offset. Others ignore it.
+ * May be null to indicate that no offset is applied.
+ * @param {Float32Array} result A typed array to hold the computed coordinates. It must be at least of
+ * size (tileWidth + 1) x (tileHeight + 1) * 3.
+ * The points are returned in row major order, beginning with the row of minimum latitude.
+ * @returns {Float32Array} The specified result argument, populated with the computed Cartesian coordinates.
+ * @throws {ArgumentError} if any of the specified globe, sector, elevations array or results arrays is null or
+ * undefined.
+ */
+GeographicProjection.prototype.geographicToCartesianGrid = function (globe, sector, numLat, numLon, elevations,
+ referencePoint, offset, result) {
+ throw new UnsupportedOperationError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "GeographicProjection", "geographicToCartesianGrid", "abstractInvocation"));
+};
+
+/**
+ * Converts a Cartesian point to a geographic position.
+ * @param {Globe} globe The globe this projection is applied to.
+ * @param {number} x The X component of the Cartesian point.
+ * @param {number} y The Y component of the Cartesian point.
+ * @param {number} z The Z component of the Cartesian point.
+ * @param {Vec3} offset An offset to apply to the Cartesian output points. Typically only projections that
+ * are continuous (see [continuous]{@link GeographicProjection#continuous}) apply this offset. Others ignore it.
+ * May be null to indicate that no offset is applied.
+ * @param {Position} result A variable in which to return the computed position.
+ *
+ * @returns {Position} The specified result argument containing the computed position.
+ * @throws {ArgumentError} If either the specified globe or result argument is null or undefined.
+ */
+GeographicProjection.prototype.cartesianToGeographic = function (globe, x, y, z, offset, result) {
+ throw new UnsupportedOperationError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "GeographicProjection", "cartesianToGeographic", "abstractInvocation"));
+};
+
+/**
+ * Computes a Cartesian vector that points north and is tangent to the meridian at a specified geographic
+ * location.
+ *
+ * @param {Globe} globe The globe this projection is applied to.
+ * @param {number} latitude The latitude of the location, in degrees.
+ * @param {number} longitude The longitude of the location, in degrees.
+ * @param {Vec3} result A variable in which to return the computed vector.
+ *
+ * @returns{Vec3} The specified result argument containing the computed vector.
+ * @throws {ArgumentError} If either the specified globe or result argument is null or undefined.
+ */
+GeographicProjection.prototype.northTangentAtLocation = function (globe, latitude, longitude, result) {
+ if (!result) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "GeographicProjection",
+ "northTangentAtLocation", "missingResult"));
+ }
+
+ result[0] = 0;
+ result[1] = 1;
+ result[2] = 0;
+
+ return result;
+};
+
+/**
+ * Computes a Cartesian vector that points north and is tangent to the meridian at a specified Cartesian
+ * point.
+ *
+ * @param {Globe} globe The globe this projection is applied to.
+ * @param {number} x The X component of the Cartesian point.
+ * @param {number} y The Y component of the Cartesian point.
+ * @param {number} z The Z component of the Cartesian point.
+ * @param {Vec3} offset An offset to apply to the Cartesian point. Typically only projections that
+ * are continuous (see [continuous]{@link GeographicProjection#continuous}) apply this offset. Others ignore it.
+ * May be null to indicate that no offset is applied.
+ * @param {Vec3} result A variable in which to return the computed vector.
+ *
+ * @returns{Vec3} The specified result argument containing the computed vector.
+ * @throws {ArgumentError} If either the specified globe or result argument is null or undefined.
+ */
+GeographicProjection.prototype.northTangentAtPoint = function (globe, x, y, z, offset, result) {
+ if (!result) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "GeographicProjection",
+ "northTangentAtPoint", "missingResult"));
+ }
+
+ result[0] = 0;
+ result[1] = 1;
+ result[2] = 0;
+
+ return result;
+};
+
+/**
+ * Computes the Cartesian surface normal vector at a specified geographic location.
+ *
+ * @param {Globe} globe The globe this projection is applied to.
+ * @param {number} latitude The latitude of the location, in degrees.
+ * @param {number} longitude The longitude of the location, in degrees.
+ * @param {Vec3} result A variable in which to return the computed vector.
+ *
+ * @returns{Vec3} The specified result argument containing the computed vector.
+ * @throws {ArgumentError} If either the specified globe or result argument is null or undefined.
+ */
+GeographicProjection.prototype.surfaceNormalAtLocation = function (globe, latitude, longitude, result) {
+ if (!result) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "GeographicProjection", "surfaceNormalAtLocation",
+ "missingResult"));
+ }
+
+ result[0] = 0;
+ result[1] = 0;
+ result[2] = 1;
+
+ return result;
+};
+
+/**
+ * Computes the Cartesian surface normal vector at a specified Cartesian point.
+ *
+ * @param {Globe} globe The globe this projection is applied to.
+ * @param {number} x The X component of the Cartesian point.
+ * @param {number} y The Y component of the Cartesian point.
+ * @param {number} z The Z component of the Cartesian point.
+ * @param {Vec3} result A variable in which to return the computed vector.
+ *
+ * @returns{Vec3} The specified result argument containing the computed vector.
+ * @throws {ArgumentError} If either the specified globe or result argument is null or undefined.
+ */
+GeographicProjection.prototype.surfaceNormalAtPoint = function (globe, x, y, z, result) {
+ if (!result) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "GeographicProjection", "surfaceNormalAtPoint",
+ "missingResult"));
+ }
+
+ result[0] = 0;
+ result[1] = 0;
+ result[2] = 1;
+
+ return result;
+};
+
+export default GeographicProjection;
diff --git a/web/test/WebWorldWind/src/projections/ProjectionWgs84.js b/web/test/WebWorldWind/src/projections/ProjectionWgs84.js
new file mode 100644
index 00000000..fde291ba
--- /dev/null
+++ b/web/test/WebWorldWind/src/projections/ProjectionWgs84.js
@@ -0,0 +1,311 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports ProjectionWgs84
+ */
+import Angle from '../geom/Angle';
+import ArgumentError from '../error/ArgumentError';
+import GeographicProjection from '../projections/GeographicProjection';
+import Logger from '../util/Logger';
+import Position from '../geom/Position';
+import Vec3 from '../geom/Vec3';
+import WWMath from '../util/WWMath';
+
+
+/**
+ * Constructs a WGS84 ellipsoid
+ * @alias ProjectionWgs84
+ * @constructor
+ * @augments GeographicProjection
+ * @classdesc Represents a WGS84 ellipsoid.
+ */
+function ProjectionWgs84() {
+
+ GeographicProjection.call(this, "WGS84", false, null);
+
+ this.is2D = false;
+
+ this.scratchPosition = new Position(0, 0, 0);
+}
+
+ProjectionWgs84.prototype = Object.create(GeographicProjection.prototype);
+
+Object.defineProperties(ProjectionWgs84.prototype, {
+ /**
+ * A string identifying this projection's current state. Used to compare states during rendering to
+ * determine whether globe-state dependent cached values must be updated. Applications typically do not
+ * interact with this property.
+ * @memberof GeographicProjection.prototype
+ * @readonly
+ * @type {String}
+ */
+ stateKey: {
+ get: function () {
+ return "projection wgs84 ";
+ }
+ }
+});
+
+// Documented in base class.
+ProjectionWgs84.prototype.geographicToCartesian = function (globe, latitude, longitude, altitude, offset,
+ result) {
+ if (!globe) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "ProjectionWgs84",
+ "geographicToCartesian", "missingGlobe"));
+ }
+
+ // latitude = WWMath._mercatorLatInvert(latitude); // TODO
+
+ var cosLat = Math.cos(latitude * Angle.DEGREES_TO_RADIANS),
+ sinLat = Math.sin(latitude * Angle.DEGREES_TO_RADIANS),
+ cosLon = Math.cos(longitude * Angle.DEGREES_TO_RADIANS),
+ sinLon = Math.sin(longitude * Angle.DEGREES_TO_RADIANS),
+ rpm = globe.equatorialRadius / Math.sqrt(1.0 - globe.eccentricitySquared * sinLat * sinLat);
+
+ result[0] = (rpm + altitude) * cosLat * sinLon;
+ result[1] = (rpm * (1.0 - globe.eccentricitySquared) + altitude) * sinLat;
+ result[2] = (rpm + altitude) * cosLat * cosLon;
+
+ return result;
+};
+
+// Documented in base class.
+ProjectionWgs84.prototype.geographicToCartesianGrid = function (globe, sector, numLat, numLon, elevations,
+ referencePoint, offset, result) {
+ if (!globe) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "ProjectionWgs84",
+ "geographicToCartesianGrid", "missingGlobe"));
+ }
+
+ var minLat = sector.minLatitude * Angle.DEGREES_TO_RADIANS,
+ maxLat = sector.maxLatitude * Angle.DEGREES_TO_RADIANS,
+ minLon = sector.minLongitude * Angle.DEGREES_TO_RADIANS,
+ maxLon = sector.maxLongitude * Angle.DEGREES_TO_RADIANS,
+ deltaLat = (maxLat - minLat) / (numLat > 1 ? numLat - 1 : 1),
+ deltaLon = (maxLon - minLon) / (numLon > 1 ? numLon - 1 : 1),
+ refCenter = referencePoint ? referencePoint : new Vec3(0, 0, 0),
+ latIndex, lonIndex,
+ elevIndex = 0, resultIndex = 0,
+ lat, lon, rpm, elev,
+ cosLat, sinLat,
+ cosLon = new Float64Array(numLon), sinLon = new Float64Array(numLon);
+
+ // Compute and save values that are a function of each unique longitude value in the specified sector. This
+ // eliminates the need to re-compute these values for each column of constant longitude.
+ for (lonIndex = 0, lon = minLon; lonIndex < numLon; lonIndex++, lon += deltaLon) {
+ if (lonIndex === numLon - 1) {
+ lon = maxLon; // explicitly set the last lon to the max longitude to ensure alignment
+ }
+
+ cosLon[lonIndex] = Math.cos(lon);
+ sinLon[lonIndex] = Math.sin(lon);
+ }
+
+ // Iterate over the latitude and longitude coordinates in the specified sector, computing the Cartesian
+ // point corresponding to each latitude and longitude.
+ for (latIndex = 0, lat = minLat; latIndex < numLat; latIndex++, lat += deltaLat) {
+ if (latIndex === numLat - 1) {
+ lat = maxLat; // explicitly set the last lat to the max longitude to ensure alignment
+ }
+
+ var invertLat = WWMath._mercatorLatInvert(lat); // TODO
+
+ // Latitude is constant for each row. Values that are a function of latitude can be computed once per row.
+ cosLat = Math.cos(invertLat);
+ sinLat = Math.sin(invertLat);
+ rpm = globe.equatorialRadius / Math.sqrt(1.0 - globe.eccentricitySquared * sinLat * sinLat);
+
+ for (lonIndex = 0; lonIndex < numLon; lonIndex++) {
+ elev = elevations[elevIndex++];
+ result[resultIndex++] = (rpm + elev) * cosLat * sinLon[lonIndex] - refCenter[0];
+ result[resultIndex++] = (rpm * (1.0 - globe.eccentricitySquared) + elev) * sinLat - refCenter[1];
+ result[resultIndex++] = (rpm + elev) * cosLat * cosLon[lonIndex] - refCenter[2];
+ }
+ }
+
+ return result;
+};
+
+// Documented in base class.
+ProjectionWgs84.prototype.cartesianToGeographic = function (globe, x, y, z, offset, result) {
+ if (!globe) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "ProjectionWgs84",
+ "cartesianToGeographic", "missingGlobe"));
+ }
+
+ // According to H. Vermeille, "An analytical method to transform geocentric into geodetic coordinates"
+ // http://www.springerlink.com/content/3t6837t27t351227/fulltext.pdf
+ // Journal of Geodesy, accepted 10/2010, not yet published
+ var X = z,
+ Y = x,
+ Z = y,
+ XXpYY = X * X + Y * Y,
+ sqrtXXpYY = Math.sqrt(XXpYY),
+ a = globe.equatorialRadius,
+ ra2 = 1 / (a * a),
+ e2 = globe.eccentricitySquared,
+ e4 = e2 * e2,
+ p = XXpYY * ra2,
+ q = Z * Z * (1 - e2) * ra2,
+ r = (p + q - e4) / 6,
+ h,
+ phi,
+ u,
+ evoluteBorderTest = 8 * r * r * r + e4 * p * q,
+ rad1,
+ rad2,
+ rad3,
+ atan,
+ v,
+ w,
+ k,
+ D,
+ sqrtDDpZZ,
+ e,
+ lambda,
+ s2;
+
+ if (evoluteBorderTest > 0 || q != 0) {
+ if (evoluteBorderTest > 0) {
+ // Step 2: general case
+ rad1 = Math.sqrt(evoluteBorderTest);
+ rad2 = Math.sqrt(e4 * p * q);
+
+ // 10*e2 is my arbitrary decision of what Vermeille means by "near... the cusps of the evolute".
+ if (evoluteBorderTest > 10 * e2) {
+ rad3 = WWMath.cbrt((rad1 + rad2) * (rad1 + rad2));
+ u = r + 0.5 * rad3 + 2 * r * r / rad3;
+ }
+ else {
+ u = r + 0.5 * WWMath.cbrt((rad1 + rad2) * (rad1 + rad2))
+ + 0.5 * WWMath.cbrt((rad1 - rad2) * (rad1 - rad2));
+ }
+ }
+ else {
+ // Step 3: near evolute
+ rad1 = Math.sqrt(-evoluteBorderTest);
+ rad2 = Math.sqrt(-8 * r * r * r);
+ rad3 = Math.sqrt(e4 * p * q);
+ atan = 2 * Math.atan2(rad3, rad1 + rad2) / 3;
+
+ u = -4 * r * Math.sin(atan) * Math.cos(Math.PI / 6 + atan);
+ }
+
+ v = Math.sqrt(u * u + e4 * q);
+ w = e2 * (u + v - q) / (2 * v);
+ k = (u + v) / (Math.sqrt(w * w + u + v) + w);
+ D = k * sqrtXXpYY / (k + e2);
+ sqrtDDpZZ = Math.sqrt(D * D + Z * Z);
+
+ h = (k + e2 - 1) * sqrtDDpZZ / k;
+ phi = 2 * Math.atan2(Z, sqrtDDpZZ + D);
+ }
+ else {
+ // Step 4: singular disk
+ rad1 = Math.sqrt(1 - e2);
+ rad2 = Math.sqrt(e2 - p);
+ e = Math.sqrt(e2);
+
+ h = -a * rad1 * rad2 / e;
+ phi = rad2 / (e * rad2 + rad1 * Math.sqrt(p));
+ }
+
+ // Compute lambda
+ s2 = Math.sqrt(2);
+ if ((s2 - 1) * Y < sqrtXXpYY + X) {
+ // case 1 - -135deg < lambda < 135deg
+ lambda = 2 * Math.atan2(Y, sqrtXXpYY + X);
+ }
+ else if (sqrtXXpYY + Y < (s2 + 1) * X) {
+ // case 2 - -225deg < lambda < 45deg
+ lambda = -Math.PI * 0.5 + 2 * Math.atan2(X, sqrtXXpYY - Y);
+ }
+ else {
+ // if (sqrtXXpYY-Y<(s2=1)*X) { // is the test, if needed, but it's not
+ // case 3: - -45deg < lambda < 225deg
+ lambda = Math.PI * 0.5 - 2 * Math.atan2(X, sqrtXXpYY + Y);
+ }
+
+ result.latitude = Angle.RADIANS_TO_DEGREES * phi;
+ result.longitude = Angle.RADIANS_TO_DEGREES * lambda;
+ result.altitude = h;
+
+ return result;
+};
+
+ProjectionWgs84.prototype.northTangentAtLocation = function (globe, latitude, longitude, result) {
+ // The north-pointing tangent is derived by rotating the vector (0, 1, 0) about the Y-axis by longitude degrees,
+ // then rotating it about the X-axis by -latitude degrees. The latitude angle must be inverted because latitude
+ // is a clockwise rotation about the X-axis, and standard rotation matrices assume counter-clockwise rotation.
+ // The combined rotation can be represented by a combining two rotation matrices Rlat, and Rlon, then
+ // transforming the vector (0, 1, 0) by the combined transform:
+ //
+ // NorthTangent = (Rlon * Rlat) * (0, 1, 0)
+ //
+ // This computation can be simplified and encoded inline by making two observations:
+ // - The vector's X and Z coordinates are always 0, and its Y coordinate is always 1.
+ // - Inverting the latitude rotation angle is equivalent to inverting sinLat. We know this by the
+ // trigonometric identities cos(-x) = cos(x), and sin(-x) = -sin(x).
+
+ var cosLat = Math.cos(latitude * Angle.DEGREES_TO_RADIANS),
+ cosLon = Math.cos(longitude * Angle.DEGREES_TO_RADIANS),
+ sinLat = Math.sin(latitude * Angle.DEGREES_TO_RADIANS),
+ sinLon = Math.sin(longitude * Angle.DEGREES_TO_RADIANS);
+
+ result[0] = -sinLat * sinLon;
+ result[1] = cosLat;
+ result[2] = -sinLat * cosLon;
+
+ return result.normalize();
+};
+
+ProjectionWgs84.prototype.northTangentAtPoint = function (globe, x, y, z, offset, result) {
+ this.cartesianToGeographic(globe, x, y, z, Vec3.ZERO, this.scratchPosition);
+
+ return this.northTangentAtLocation(globe, this.scratchPosition.latitude, this.scratchPosition.longitude, result);
+};
+
+ProjectionWgs84.prototype.surfaceNormalAtLocation = function (globe, latitude, longitude, result) {
+ var cosLat = Math.cos(latitude * Angle.DEGREES_TO_RADIANS),
+ cosLon = Math.cos(longitude * Angle.DEGREES_TO_RADIANS),
+ sinLat = Math.sin(latitude * Angle.DEGREES_TO_RADIANS),
+ sinLon = Math.sin(longitude * Angle.DEGREES_TO_RADIANS);
+
+ result[0] = cosLat * sinLon;
+ result[1] = sinLat;
+ result[2] = cosLat * cosLon;
+
+ return result.normalize();
+};
+
+ProjectionWgs84.prototype.surfaceNormalAtPoint = function (globe, x, y, z, result) {
+ if (!globe) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "ProjectionWgs84",
+ "surfaceNormalAtPoint", "missingGlobe"));
+ }
+
+ var a2 = globe.equatorialRadius * globe.equatorialRadius,
+ b2 = globe.polarRadius * globe.polarRadius;
+
+ result[0] = x / a2;
+ result[1] = y / b2;
+ result[2] = z / a2;
+
+ return result.normalize();
+};
+
+export default ProjectionWgs84;
diff --git a/web/test/WebWorldWind/src/render/DrawContext.js b/web/test/WebWorldWind/src/render/DrawContext.js
new file mode 100644
index 00000000..88c3e336
--- /dev/null
+++ b/web/test/WebWorldWind/src/render/DrawContext.js
@@ -0,0 +1,1567 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports DrawContext
+ */
+import ArgumentError from '../error/ArgumentError';
+import Color from '../util/Color';
+import FramebufferTexture from '../render/FramebufferTexture';
+import FramebufferTileController from '../render/FramebufferTileController';
+import Frustum from '../geom/Frustum';
+import GpuResourceCache from '../cache/GpuResourceCache';
+import Logger from '../util/Logger';
+import Matrix from '../geom/Matrix';
+import PickedObjectList from '../pick/PickedObjectList';
+import Plane from '../geom/Plane';
+import Position from '../geom/Position';
+import Rectangle from '../geom/Rectangle';
+import ScreenCreditController from '../render/ScreenCreditController';
+import SurfaceShape from '../shapes/SurfaceShape';
+import SurfaceShapeTileBuilder from '../shapes/SurfaceShapeTileBuilder';
+import SurfaceTileRenderer from '../render/SurfaceTileRenderer';
+import TextRenderer from '../render/TextRenderer';
+import Vec2 from '../geom/Vec2';
+import Vec3 from '../geom/Vec3';
+import WWMath from '../util/WWMath';
+
+
+/**
+ * Constructs a DrawContext. Applications do not call this constructor. A draw context is created by a
+ * {@link WorldWindow} during its construction.
+ * @alias DrawContext
+ * @constructor
+ * @classdesc Provides current state during rendering. The current draw context is passed to most rendering
+ * methods in order to make those methods aware of current state.
+ * @param {WebGLRenderingContext} gl The WebGL rendering context this draw context is associated with.
+ * @throws {ArgumentError} If the specified WebGL rendering context is null or undefined.
+ */
+function DrawContext(gl) {
+ if (!gl) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Texture", "constructor",
+ "missingGlContext"));
+ }
+
+ /**
+ * The current WebGL rendering context.
+ * @type {WebGLRenderingContext}
+ */
+ this.currentGlContext = gl;
+
+ /**
+ * A 2D canvas for creating texture maps.
+ * @type {HTMLElement}
+ */
+ this.canvas2D = document.createElement("canvas");
+
+ /**
+ * A 2D context for this draw context's [canvas property]{@link DrawContext#canvas}.
+ */
+ this.ctx2D = this.canvas2D.getContext("2d");
+
+ /**
+ * The current clear color.
+ * @type {Color}
+ * @default Color.TRANSPARENT (red = 0, green = 0, blue = 0, alpha = 0)
+ */
+ this.clearColor = Color.TRANSPARENT;
+
+ /**
+ * The GPU resource cache, which tracks WebGL resources.
+ * @type {GpuResourceCache}
+ */
+ this.gpuResourceCache = new GpuResourceCache(WorldWind.configuration.gpuCacheSize,
+ 0.8 * WorldWind.configuration.gpuCacheSize);
+
+ /**
+ * The surface-tile-renderer to use for drawing surface tiles.
+ * @type {SurfaceTileRenderer}
+ */
+ this.surfaceTileRenderer = new SurfaceTileRenderer();
+
+ /**
+ * The surface shape tile builder used to create and draw surface shapes.
+ * @type {SurfaceShapeTileBuilder}
+ */
+ this.surfaceShapeTileBuilder = new SurfaceShapeTileBuilder();
+
+ /**
+ * Provides access to a multi-resolution WebGL framebuffer arranged as adjacent tiles in a pyramid. Surface
+ * shapes use these tiles internally to draw on the terrain surface.
+ * @type {FramebufferTileController}
+ */
+ this.surfaceShapeTileController = new FramebufferTileController();
+
+ /**
+ * The screen credit controller responsible for collecting and drawing screen credits.
+ * @type {ScreenCreditController}
+ */
+ this.screenCreditController = new ScreenCreditController();
+
+ /**
+ * A shared TextRenderer instance.
+ * @type {TextRenderer}
+ */
+ this.textRenderer = new TextRenderer(this);
+
+ /**
+ * The current WebGL framebuffer. Null indicates that the default WebGL framebuffer is active.
+ * @type {FramebufferTexture}
+ */
+ this.currentFramebuffer = null;
+
+ /**
+ * The current WebGL program. Null indicates that no WebGL program is active.
+ * @type {GpuProgram}
+ */
+ this.currentProgram = null;
+
+ /**
+ * The list of surface renderables.
+ * @type {Array}
+ */
+ this.surfaceRenderables = [];
+
+ /**
+ * Indicates whether this draw context is in ordered rendering mode.
+ * @type {Boolean}
+ */
+ this.orderedRenderingMode = false;
+
+ /**
+ * The list of ordered renderables.
+ * @type {Array}
+ */
+ this.orderedRenderables = [];
+
+ // Internal. Intentionally not documented. Provides ordinal IDs to ordered renderables.
+ this.orderedRenderablesCounter = 0; // Number
+
+ /**
+ * The starting time of the current frame, in milliseconds. The frame timestamp is updated immediately
+ * before the WorldWindow associated with this draw context is rendered, either as a result of redrawing or
+ * as a result of a picking operation.
+ * @type {Number}
+ * @readonly
+ */
+ this.timestamp = Date.now();
+
+ /**
+ * The [time stamp]{@link DrawContext#timestamp} of the last visible frame, in milliseconds. This indicates
+ * the time stamp that was current during the WorldWindow's last frame, ignoring frames associated with a
+ * picking operation. The difference between the previous redraw time stamp and the current time stamp
+ * indicates the duration between visible frames, e.g. timeStamp - previousRedrawTimestamp.
+ * @type {Number}
+ * @readonly
+ */
+ this.previousRedrawTimestamp = this.timestamp;
+
+ /**
+ * Indicates whether a redraw has been requested during the current frame. When true, this causes the World
+ * Window associated with this draw context to redraw after the current frame.
+ * @type {Boolean}
+ */
+ this.redrawRequested = false;
+
+ /**
+ * The globe being rendered.
+ * @type {Globe}
+ */
+ this.globe = null;
+
+ /**
+ * A copy of the current globe's state key. Provided here to avoid having to recompute it every time
+ * it's needed.
+ * @type {String}
+ */
+ this.globeStateKey = null;
+
+ /**
+ * The layers being rendered.
+ * @type {Layer[]}
+ */
+ this.layers = null;
+
+ /**
+ * The layer being rendered.
+ * @type {Layer}
+ */
+ this.currentLayer = null;
+
+ /**
+ * The current eye position.
+ * @type {Position}
+ */
+ this.eyePosition = new Position(0, 0, 0);
+
+ /**
+ * The eye point in model coordinates, relative to the globe's center.
+ * @type {Vec3}
+ * @readonly
+ */
+ this.eyePoint = new Vec3(0, 0, 0);
+
+ /**
+ * The current screen projection matrix.
+ * @type {Matrix}
+ */
+ this.screenProjection = Matrix.fromIdentity();
+
+ /**
+ * The terrain for the current frame.
+ * @type {Terrain}
+ */
+ this.terrain = null;
+
+ /**
+ * The current vertical exaggeration.
+ * @type {Number}
+ */
+ this.verticalExaggeration = 1;
+
+ /**
+ * The number of milliseconds over which to fade shapes that support fading. Fading is most typically
+ * used during decluttering.
+ * @type {Number}
+ * @default 500
+ */
+ this.fadeTime = 500;
+
+ /**
+ * The opacity to apply to terrain and surface shapes. Should be a number between 0 and 1.
+ * @type {Number}
+ * @default 1
+ */
+ this.surfaceOpacity = 1;
+
+ /**
+ * Frame statistics.
+ * @type {FrameStatistics}
+ */
+ this.frameStatistics = null;
+
+ /**
+ * Indicates whether the frame is being drawn for picking.
+ * @type {Boolean}
+ */
+ this.pickingMode = false;
+
+ /**
+ * Indicates that picking will return only the terrain object, if the pick point is over the terrain.
+ * @type {Boolean}
+ * @default false
+ */
+ this.pickTerrainOnly = false;
+
+ /**
+ * Indicates that picking will return all objects at the pick point, if any. The top-most object will have
+ * its isOnTop flag set to true. If [deep picking]{@link WorldWindow#deepPicking} is false, the default,
+ * only the top-most object is returned, plus the picked-terrain object if the pick point is over the
+ * terrain.
+ * @type {Boolean}
+ * @default false
+ */
+ this.deepPicking = false;
+
+ /**
+ * Indicates that picking will return all objects that intersect the pick region, if any. Visible objects
+ * will have the isOnTop flag set to true.
+ * @type {Boolean}
+ * @default false
+ */
+ this.regionPicking = false;
+
+ /**
+ * The current pick point, in screen coordinates.
+ * @type {Vec2}
+ */
+ this.pickPoint = null;
+
+ /**
+ * The current pick ray originating at the eyePoint and extending through the pick point.
+ * @type {Line}
+ */
+ this.pickRay = null;
+
+ /**
+ * The current pick rectangle, in WebGL (lower-left origin) screen coordinates.
+ * @type {Rectangle}
+ */
+ this.pickRectangle = null;
+
+ /**
+ * The off-screen WebGL framebuffer used during picking.
+ * @type {FramebufferTexture}
+ * @readonly
+ */
+ this.pickFramebuffer = null;
+
+ /**
+ * The current pick frustum, created anew each picking frame.
+ * @type {Frustum}
+ * @readonly
+ */
+ this.pickFrustum = null;
+
+ // Internal. Keeps track of the current pick color.
+ this.pickColor = new Color(0, 0, 0, 1);
+
+ /**
+ * The objects at the current pick point.
+ * @type {PickedObjectList}
+ * @readonly
+ */
+ this.objectsAtPickPoint = new PickedObjectList();
+
+ // Intentionally not documented.
+ this.pixelScale = 1;
+
+ // TODO: replace with camera in the next phase of navigator refactoring
+ this.navigator = null;
+
+ /**
+ * The model-view matrix. The model-view matrix transforms points from model coordinates to eye
+ * coordinates.
+ * @type {Matrix}
+ * @readonly
+ */
+ this.modelview = Matrix.fromIdentity();
+
+ /**
+ * The projection matrix. The projection matrix transforms points from eye coordinates to clip
+ * coordinates.
+ * @type {Matrix}
+ * @readonly
+ */
+ this.projection = Matrix.fromIdentity();
+
+ /**
+ * The concatenation of the DrawContext's model-view and projection matrices. This matrix transforms points
+ * from model coordinates to clip coordinates.
+ * @type {Matrix}
+ * @readonly
+ */
+ this.modelviewProjection = Matrix.fromIdentity();
+
+ /**
+ * The viewing frustum in model coordinates. The frustum originates at the eyePoint and extends
+ * outward along the forward vector. The near distance and far distance identify the minimum and
+ * maximum distance, respectively, at which an object in the scene is visible.
+ * @type {Frustum}
+ * @readonly
+ */
+ this.frustumInModelCoordinates = null;
+
+ /**
+ * The matrix that transforms normal vectors in model coordinates to normal vectors in eye coordinates.
+ * Typically used to transform a shape's normal vectors during lighting calculations.
+ * @type {Matrix}
+ * @readonly
+ */
+ this.modelviewNormalTransform = Matrix.fromIdentity();
+
+ /**
+ * The current viewport.
+ * @type {Rectangle}
+ * @readonly
+ */
+ this.viewport = new Rectangle(0, 0, 0, 0);
+
+ // Intentionally not documented.
+ this.pixelSizeFactor = 0;
+
+ // Intentionally not documented.
+ this.pixelSizeOffset = 0;
+
+ // Intentionally not documented.
+ this.glExtensionsCache = {};
+}
+
+// Internal use. Intentionally not documented.
+DrawContext.unitCubeKey = "DrawContextUnitCubeKey";
+DrawContext.unitCubeElementsKey = "DrawContextUnitCubeElementsKey";
+DrawContext.unitQuadKey = "DrawContextUnitQuadKey";
+DrawContext.unitQuadKey3 = "DrawContextUnitQuadKey3";
+
+/**
+ * Prepare this draw context for the drawing of a new frame.
+ */
+DrawContext.prototype.reset = function () {
+ // Reset the draw context's internal properties.
+ this.screenCreditController.clear();
+ this.surfaceRenderables = []; // clears the surface renderables array
+ this.orderedRenderingMode = false;
+ this.orderedRenderables = []; // clears the ordered renderables array
+ this.screenRenderables = [];
+ this.orderedRenderablesCounter = 0;
+
+ // Advance the per-frame timestamp.
+ var previousTimestamp = this.timestamp;
+ this.timestamp = Date.now();
+ if (this.timestamp === previousTimestamp)
+ ++this.timestamp;
+
+ // Reset properties set by the WorldWindow every frame.
+ this.redrawRequested = false;
+ this.globe = null;
+ this.globeStateKey = null;
+ this.layers = null;
+ this.currentLayer = null;
+ this.terrain = null;
+ this.verticalExaggeration = 1;
+ this.frameStatistics = null;
+ this.accumulateOrderedRenderables = true;
+
+ // Reset picking properties that may be set by the WorldWindow.
+ this.pickingMode = false;
+ this.pickTerrainOnly = false;
+ this.deepPicking = false;
+ this.regionPicking = false;
+ this.pickPoint = null;
+ this.pickRay = null;
+ this.pickRectangle = null;
+ this.pickFrustum = null;
+ this.pickColor = new Color(0, 0, 0, 1);
+ this.objectsAtPickPoint.clear();
+
+ this.eyePoint.set(0, 0, 0);
+ this.modelview.setToIdentity();
+ this.projection.setToIdentity();
+ this.modelviewProjection.setToIdentity();
+ this.frustumInModelCoordinates = null;
+ this.modelviewNormalTransform.setToIdentity();
+};
+
+/**
+ * Computes any values necessary to render the upcoming frame. Called after all draw context state for the
+ * frame has been set.
+ */
+DrawContext.prototype.update = function () {
+ var gl = this.currentGlContext,
+ eyePoint = this.eyePoint;
+
+ this.globeStateKey = this.globe.stateKey;
+ this.globe.computePositionFromPoint(eyePoint[0], eyePoint[1], eyePoint[2], this.eyePosition);
+ this.screenProjection.setToScreenProjection(gl.drawingBufferWidth, gl.drawingBufferHeight);
+};
+
+/**
+ * Notifies this draw context that the current WebGL rendering context has been lost. This function removes all
+ * cached WebGL resources and resets all properties tracking the current WebGL state.
+ */
+DrawContext.prototype.contextLost = function () {
+ // Remove all cached WebGL resources, which are now invalid.
+ this.gpuResourceCache.clear();
+ this.pickFramebuffer = null;
+ // Reset properties tracking the current WebGL state, which are now invalid.
+ this.currentFramebuffer = null;
+ this.currentProgram = null;
+ this.glExtensionsCache = {};
+};
+
+/**
+ * Notifies this draw context that the current WebGL rendering context has been restored. This function prepares
+ * this draw context to resume rendering.
+ */
+DrawContext.prototype.contextRestored = function () {
+ // Remove all cached WebGL resources. This cache is already cleared when the context is lost, but
+ // asynchronous load operations that complete between context lost and context restored populate the cache
+ // with invalid entries.
+ this.gpuResourceCache.clear();
+ this.glExtensionsCache = {};
+};
+
+/**
+ * Binds a specified WebGL framebuffer. This function also makes the framebuffer the active framebuffer.
+ * @param {FramebufferTexture} framebuffer The framebuffer to bind. May be null or undefined, in which case the
+ * default WebGL framebuffer is made active.
+ */
+DrawContext.prototype.bindFramebuffer = function (framebuffer) {
+ if (this.currentFramebuffer != framebuffer) {
+ this.currentGlContext.bindFramebuffer(this.currentGlContext.FRAMEBUFFER,
+ framebuffer ? framebuffer.framebufferId : null);
+ this.currentFramebuffer = framebuffer;
+ }
+};
+
+/**
+ * Binds a specified WebGL program. This function also makes the program the current program.
+ * @param {GpuProgram} program The program to bind. May be null or undefined, in which case the currently
+ * bound program is unbound.
+ */
+DrawContext.prototype.bindProgram = function (program) {
+ if (this.currentProgram != program) {
+ this.currentGlContext.useProgram(program ? program.programId : null);
+ this.currentProgram = program;
+ }
+};
+
+/**
+ * Binds a potentially cached WebGL program, creating and caching it if it isn't already cached.
+ * This function also makes the program the current program.
+ * @param {function} programConstructor The constructor to use to create the program.
+ * @returns {GpuProgram} The bound program.
+ * @throws {ArgumentError} If the specified constructor is null or undefined.
+ */
+DrawContext.prototype.findAndBindProgram = function (programConstructor) {
+ if (!programConstructor) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "DrawContext", "findAndBindProgram",
+ "The specified program constructor is null or undefined."));
+ }
+
+ var program = this.gpuResourceCache.resourceForKey(programConstructor.key);
+ if (program) {
+ this.bindProgram(program);
+ } else {
+ try {
+ program = new programConstructor(this.currentGlContext);
+ this.bindProgram(program);
+ this.gpuResourceCache.putResource(programConstructor.key, program, program.size);
+ } catch (e) {
+ Logger.log(Logger.LEVEL_SEVERE, "Error attempting to create GPU program.");
+ }
+ }
+
+ return program;
+};
+
+/**
+ * Adds a surface renderable to this draw context's surface renderable list.
+ * @param {SurfaceRenderable} surfaceRenderable The surface renderable to add. May be null, in which case the
+ * current surface renderable list remains unchanged.
+ */
+DrawContext.prototype.addSurfaceRenderable = function (surfaceRenderable) {
+ if (surfaceRenderable) {
+ this.surfaceRenderables.push(surfaceRenderable);
+ }
+};
+
+/**
+ * Returns the surface renderable at the head of the surface renderable list without removing it from the list.
+ * @returns {SurfaceRenderable} The first surface renderable in this draw context's surface renderable list, or
+ * null if the surface renderable list is empty.
+ */
+DrawContext.prototype.peekSurfaceRenderable = function () {
+ if (this.surfaceRenderables.length > 0) {
+ return this.surfaceRenderables[this.surfaceRenderables.length - 1];
+ } else {
+ return null;
+ }
+};
+
+/**
+ * Returns the surface renderable at the head of the surface renderable list and removes it from the list.
+ * @returns {SurfaceRenderable} The first surface renderable in this draw context's surface renderable list, or
+ * null if the surface renderable list is empty.
+ */
+DrawContext.prototype.popSurfaceRenderable = function () {
+ if (this.surfaceRenderables.length > 0) {
+ return this.surfaceRenderables.pop();
+ } else {
+ return null;
+ }
+};
+
+/**
+ * Reverses the surface renderable list in place. After this function completes, the functions
+ * peekSurfaceRenderable and popSurfaceRenderable return renderables in the order in which they were added to
+ * the surface renderable list.
+ */
+DrawContext.prototype.reverseSurfaceRenderables = function () {
+ this.surfaceRenderables.reverse();
+};
+
+/**
+ * Adds an ordered renderable to this draw context's ordered renderable list.
+ * @param {OrderedRenderable} orderedRenderable The ordered renderable to add. May be null, in which case the
+ * current ordered renderable list remains unchanged.
+ * @param {Number} eyeDistance An optional argument indicating the ordered renderable's eye distance.
+ * If this parameter is not specified then the ordered renderable must have an eyeDistance property.
+ */
+DrawContext.prototype.addOrderedRenderable = function (orderedRenderable, eyeDistance) {
+ if (orderedRenderable) {
+ var ore = {
+ orderedRenderable: orderedRenderable,
+ insertionOrder: this.orderedRenderablesCounter++,
+ eyeDistance: eyeDistance || orderedRenderable.eyeDistance,
+ globeStateKey: this.globeStateKey
+ };
+
+ if (this.globe.continuous) {
+ ore.globeOffset = this.globe.offset;
+ }
+
+ if (ore.eyeDistance === 0) {
+ this.screenRenderables.push(ore);
+ } else {
+ this.orderedRenderables.push(ore);
+ }
+ }
+};
+
+/**
+ * Adds an ordered renderable to the end of this draw context's ordered renderable list, denoting it as the
+ * most distant from the eye point.
+ * @param {OrderedRenderable} orderedRenderable The ordered renderable to add. May be null, in which case the
+ * current ordered renderable list remains unchanged.
+ */
+DrawContext.prototype.addOrderedRenderableToBack = function (orderedRenderable) {
+ if (orderedRenderable) {
+ var ore = {
+ orderedRenderable: orderedRenderable,
+ insertionOrder: this.orderedRenderablesCounter++,
+ eyeDistance: Number.MAX_VALUE,
+ globeStateKey: this.globeStateKey
+ };
+
+ if (this.globe.continuous) {
+ ore.globeOffset = this.globe.offset;
+ }
+
+ this.orderedRenderables.push(ore);
+ }
+};
+
+/**
+ * Returns the ordered renderable at the head of the ordered renderable list without removing it from the list.
+ * @returns {OrderedRenderable} The first ordered renderable in this draw context's ordered renderable list, or
+ * null if the ordered renderable list is empty.
+ */
+DrawContext.prototype.peekOrderedRenderable = function () {
+ if (this.orderedRenderables.length > 0) {
+ return this.orderedRenderables[this.orderedRenderables.length - 1].orderedRenderable;
+ } else {
+ return null;
+ }
+};
+
+/**
+ * Returns the ordered renderable at the head of the ordered renderable list and removes it from the list.
+ * @returns {OrderedRenderable} The first ordered renderable in this draw context's ordered renderable list, or
+ * null if the ordered renderable list is empty.
+ */
+DrawContext.prototype.popOrderedRenderable = function () {
+ if (this.orderedRenderables.length > 0) {
+ var ore = this.orderedRenderables.pop();
+ this.globeStateKey = ore.globeStateKey;
+
+ if (this.globe.continuous) {
+ // Restore the globe state to that when the ordered renderable was created.
+ this.globe.offset = ore.globeOffset;
+ }
+
+ return ore.orderedRenderable;
+ } else {
+ return null;
+ }
+};
+
+/**
+ * Returns the ordered renderable at the head of the ordered renderable list and removes it from the list.
+ * @returns {OrderedRenderable} The first ordered renderable in this draw context's ordered renderable list, or
+ * null if the ordered renderable list is empty.
+ */
+DrawContext.prototype.nextScreenRenderable = function () {
+ if (this.screenRenderables.length > 0) {
+ var ore = this.screenRenderables.shift();
+ this.globeStateKey = ore.globeStateKey;
+
+ if (this.globe.continuous) {
+ // Restore the globe state to that when the ordered renderable was created.
+ this.globe.offset = ore.globeOffset;
+ }
+
+ return ore.orderedRenderable;
+ } else {
+ return null;
+ }
+};
+
+/**
+ * Sorts the ordered renderable list from nearest to the eye point to farthest from the eye point.
+ */
+DrawContext.prototype.sortOrderedRenderables = function () {
+ // Sort the ordered renderables by eye distance from front to back and then by insertion time. The ordered
+ // renderable peek and pop access the back of the ordered renderable list, thereby causing ordered renderables to
+ // be processed from back to front.
+
+ this.orderedRenderables.sort(function (oreA, oreB) {
+ var eA = oreA.eyeDistance,
+ eB = oreB.eyeDistance;
+
+ if (eA < eB) { // orA is closer to the eye than orB; sort orA before orB
+ return -1;
+ } else if (eA > eB) { // orA is farther from the eye than orB; sort orB before orA
+ return 1;
+ } else { // orA and orB are the same distance from the eye; sort them based on insertion time
+ var tA = oreA.insertionOrder,
+ tB = oreB.insertionOrder;
+
+ if (tA > tB) {
+ return -1;
+ } else if (tA < tB) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ });
+};
+
+/**
+ * Reads the color from the current render buffer at a specified point. Used during picking to identify the item most
+ * recently affecting the pixel at the specified point.
+ * @param {Vec2} pickPoint The current pick point.
+ * @returns {Color} The color at the pick point.
+ */
+DrawContext.prototype.readPickColor = function (pickPoint) {
+ var glPickPoint = this.convertPointToViewport(pickPoint, new Vec2(0, 0)),
+ colorBytes = new Uint8Array(4);
+
+ this.currentGlContext.readPixels(glPickPoint[0], glPickPoint[1], 1, 1, this.currentGlContext.RGBA,
+ this.currentGlContext.UNSIGNED_BYTE, colorBytes);
+
+ if (this.clearColor.equalsBytes(colorBytes)) {
+ return null;
+ }
+
+ return Color.colorFromByteArray(colorBytes);
+};
+
+/**
+ * Reads the current pick buffer colors in a specified rectangle. Used during region picking to identify
+ * the items not occluded.
+ * @param {Rectangle} pickRectangle The rectangle for which to read the colors.
+ * @returns {{}} An object containing the unique colors in the specified rectangle, excluding the current
+ * clear color. The colors are referenced by their byte string
+ * (see [Color.toByteString]{@link Color#toByteString}.
+ */
+DrawContext.prototype.readPickColors = function (pickRectangle) {
+ var gl = this.currentGlContext,
+ colorBytes = new Uint8Array(pickRectangle.width * pickRectangle.height * 4),
+ uniqueColors = {},
+ color,
+ blankColor = new Color(0, 0, 0, 0),
+ packAlignment = gl.getParameter(gl.PACK_ALIGNMENT);
+
+ gl.pixelStorei(gl.PACK_ALIGNMENT, 1); // read byte aligned
+ this.currentGlContext.readPixels(pickRectangle.x, pickRectangle.y,
+ pickRectangle.width, pickRectangle.height,
+ gl.RGBA, gl.UNSIGNED_BYTE, colorBytes);
+ gl.pixelStorei(gl.PACK_ALIGNMENT, packAlignment); // restore the pack alignment
+
+ for (var i = 0, len = pickRectangle.width * pickRectangle.height; i < len; i++) {
+ var k = i * 4;
+ color = Color.colorFromBytes(colorBytes[k], colorBytes[k + 1], colorBytes[k + 2], colorBytes[k + 3]);
+ if (color.equals(this.clearColor) || color.equals(blankColor))
+ continue;
+ uniqueColors[color.toByteString()] = color;
+ }
+
+ return uniqueColors;
+};
+
+/**
+ * Determines whether a specified picked object is under the pick point, and if it is adds it to this draw
+ * context's list of picked objects. This method should be called by shapes during ordered rendering
+ * after the shape is drawn. If this draw context is in single-picking mode, the specified pickable object
+ * is added to the list of picked objects whether or not it is under the pick point.
+ * @param pickableObject
+ * @returns {null}
+ */
+DrawContext.prototype.resolvePick = function (pickableObject) {
+ if (!(pickableObject.userObject instanceof SurfaceShape) && this.deepPicking && !this.regionPicking) {
+ var color = this.readPickColor(this.pickPoint);
+ if (!color) { // getPickColor returns null if the pick point selects the clear color
+ return null;
+ }
+
+ if (pickableObject.color.equals(color)) {
+ this.addPickedObject(pickableObject);
+ }
+ } else {
+ // Don't resolve. Just add the object to the pick list. It will be resolved later.
+ this.addPickedObject(pickableObject);
+ }
+};
+
+/**
+ * Adds an object to the current picked-object list. The list identifies objects that are at the pick point
+ * but not necessarily the top-most object.
+ * @param {PickedObject} pickedObject The object to add.
+ */
+DrawContext.prototype.addPickedObject = function (pickedObject) {
+ if (pickedObject) {
+ this.objectsAtPickPoint.add(pickedObject);
+ }
+};
+
+/**
+ * Computes a unique color to use as a pick color.
+ * @returns {Color} A unique color.
+ */
+DrawContext.prototype.uniquePickColor = function () {
+ var color = this.pickColor.nextColor().clone();
+
+ return color.equals(this.clearColor) ? color.nextColor() : color;
+};
+
+/**
+ * Creates an off-screen WebGL framebuffer for use during picking and stores it in this draw context. The
+ * framebuffer width and height match the WebGL rendering context's drawingBufferWidth and drawingBufferHeight.
+ */
+DrawContext.prototype.makePickFramebuffer = function () {
+ var gl = this.currentGlContext,
+ width = gl.drawingBufferWidth,
+ height = gl.drawingBufferHeight;
+
+ if (!this.pickFramebuffer ||
+ this.pickFramebuffer.width != width ||
+ this.pickFramebuffer.height != height) {
+
+ this.pickFramebuffer = new FramebufferTexture(gl, width, height, true); // enable depth buffering
+ }
+
+ return this.pickFramebuffer;
+};
+
+/**
+ * Creates a pick frustum for the current pick point and stores it in this draw context. If this context's
+ * pick rectangle is null or undefined then a pick rectangle is also computed and assigned to this context.
+ * If the existing pick rectangle extends beyond the viewport then it is truncated by this method to fit
+ * within the viewport.
+ * This method assumes that this draw context's pick point or pick rectangle has been set. It returns
+ * false if neither one of these exists.
+ *
+ * @returns {Boolean} true if the pick frustum could be created, otherwise false.
+ */
+DrawContext.prototype.makePickFrustum = function () {
+ if (!this.pickPoint && !this.pickRectangle) {
+ return false;
+ }
+
+ var lln, llf, lrn, lrf, uln, ulf, urn, urf, // corner points of frustum
+ nl, nr, nt, nb, nn, nf, // normal vectors of frustum planes
+ l, r, t, b, n, f, // frustum planes
+ va, vb = new Vec3(0, 0, 0), // vectors formed by the corner points
+ apertureRadius = 2, // radius of pick window in screen coordinates
+ screenPoint = new Vec3(0, 0, 0),
+ pickPoint,
+ pickRectangle = this.pickRectangle,
+ viewport = this.viewport;
+
+ // Compute the pick rectangle if necessary.
+ if (!pickRectangle) {
+ pickPoint = this.convertPointToViewport(this.pickPoint, new Vec2(0, 0));
+ pickRectangle = new Rectangle(
+ pickPoint[0] - apertureRadius,
+ pickPoint[1] - apertureRadius,
+ 2 * apertureRadius,
+ 2 * apertureRadius);
+ }
+
+ // Clamp the pick rectangle to the viewport.
+
+ var xl = pickRectangle.x,
+ xr = pickRectangle.x + pickRectangle.width,
+ yb = pickRectangle.y,
+ yt = pickRectangle.y + pickRectangle.height;
+
+ if (xr < 0 || yt < 0 || xl > viewport.x + viewport.width || yb > viewport.y + viewport.height) {
+ return false; // pick rectangle is outside the viewport.
+ }
+
+ pickRectangle.x = WWMath.clamp(xl, viewport.x, viewport.x + viewport.width);
+ pickRectangle.y = WWMath.clamp(yb, viewport.y, viewport.y + viewport.height);
+ pickRectangle.width = WWMath.clamp(xr, viewport.x, viewport.x + viewport.width) - pickRectangle.x;
+ pickRectangle.height = WWMath.clamp(yt, viewport.y, viewport.y + viewport.height) - pickRectangle.y;
+ this.pickRectangle = pickRectangle;
+
+ // Compute the pick frustum.
+ var modelviewProjectionInv = Matrix.fromIdentity();
+ modelviewProjectionInv.invertMatrix(this.modelviewProjection);
+
+ screenPoint[0] = pickRectangle.x;
+ screenPoint[1] = pickRectangle.y;
+ screenPoint[2] = 0;
+ modelviewProjectionInv.unProject(screenPoint, viewport, lln = new Vec3(0, 0, 0));
+
+ screenPoint[0] = pickRectangle.x;
+ screenPoint[1] = pickRectangle.y;
+ screenPoint[2] = 1;
+ modelviewProjectionInv.unProject(screenPoint, viewport, llf = new Vec3(0, 0, 0));
+
+ screenPoint[0] = pickRectangle.x + pickRectangle.width;
+ screenPoint[1] = pickRectangle.y;
+ screenPoint[2] = 0;
+ modelviewProjectionInv.unProject(screenPoint, viewport, lrn = new Vec3(0, 0, 0));
+
+ screenPoint[0] = pickRectangle.x + pickRectangle.width;
+ screenPoint[1] = pickRectangle.y;
+ screenPoint[2] = 1;
+ modelviewProjectionInv.unProject(screenPoint, viewport, lrf = new Vec3(0, 0, 0));
+
+ screenPoint[0] = pickRectangle.x;
+ screenPoint[1] = pickRectangle.y + pickRectangle.height;
+ screenPoint[2] = 0;
+ modelviewProjectionInv.unProject(screenPoint, viewport, uln = new Vec3(0, 0, 0));
+
+ screenPoint[0] = pickRectangle.x;
+ screenPoint[1] = pickRectangle.y + pickRectangle.height;
+ screenPoint[2] = 1;
+ modelviewProjectionInv.unProject(screenPoint, viewport, ulf = new Vec3(0, 0, 0));
+
+ screenPoint[0] = pickRectangle.x + pickRectangle.width;
+ screenPoint[1] = pickRectangle.y + pickRectangle.height;
+ screenPoint[2] = 0;
+ modelviewProjectionInv.unProject(screenPoint, viewport, urn = new Vec3(0, 0, 0));
+
+ screenPoint[0] = pickRectangle.x + pickRectangle.width;
+ screenPoint[1] = pickRectangle.y + pickRectangle.height;
+ screenPoint[2] = 1;
+ modelviewProjectionInv.unProject(screenPoint, viewport, urf = new Vec3(0, 0, 0));
+
+ va = new Vec3(ulf[0] - lln[0], ulf[1] - lln[1], ulf[2] - lln[2]);
+ vb.set(uln[0] - llf[0], uln[1] - llf[1], uln[2] - llf[2]);
+ nl = va.cross(vb);
+ l = new Plane(nl[0], nl[1], nl[2], -nl.dot(lln));
+ l.normalize();
+
+ va = new Vec3(urn[0] - lrf[0], urn[1] - lrf[1], urn[2] - lrf[2]);
+ vb.set(urf[0] - lrn[0], urf[1] - lrn[1], urf[2] - lrn[2]);
+ nr = va.cross(vb);
+ r = new Plane(nr[0], nr[1], nr[2], -nr.dot(lrn));
+ r.normalize();
+
+ va = new Vec3(ulf[0] - urn[0], ulf[1] - urn[1], ulf[2] - urn[2]);
+ vb.set(urf[0] - uln[0], urf[1] - uln[1], urf[2] - uln[2]);
+ nt = va.cross(vb);
+ t = new Plane(nt[0], nt[1], nt[2], -nt.dot(uln));
+ t.normalize();
+
+ va = new Vec3(lrf[0] - lln[0], lrf[1] - lln[1], lrf[2] - lln[2]);
+ vb.set(llf[0] - lrn[0], llf[1] - lrn[1], llf[2] - lrn[2]);
+ nb = va.cross(vb);
+ b = new Plane(nb[0], nb[1], nb[2], -nb.dot(lrn));
+ b.normalize();
+
+ va = new Vec3(uln[0] - lrn[0], uln[1] - lrn[1], uln[2] - lrn[2]);
+ vb.set(urn[0] - lln[0], urn[1] - lln[1], urn[2] - lln[2]);
+ nn = va.cross(vb);
+ n = new Plane(nn[0], nn[1], nn[2], -nn.dot(lln));
+ n.normalize();
+
+ va = new Vec3(urf[0] - llf[0], urf[1] - llf[1], urf[2] - llf[2]);
+ vb.set(ulf[0] - lrf[0], ulf[1] - lrf[1], ulf[2] - lrf[2]);
+ nf = va.cross(vb);
+ f = new Plane(nf[0], nf[1], nf[2], -nf.dot(llf));
+ f.normalize();
+
+ this.pickFrustum = new Frustum(l, r, b, t, n, f);
+
+ return true;
+};
+
+/**
+ * Indicates whether an extent is smaller than a specified number of pixels.
+ * @param {BoundingBox} extent The extent to test.
+ * @param {Number} numPixels The number of pixels below which the extent is considered small.
+ * @returns {Boolean} True if the extent is smaller than the specified number of pixels, otherwise false.
+ * Returns false if the extent is null or undefined.
+ */
+DrawContext.prototype.isSmall = function (extent, numPixels) {
+ if (!extent) {
+ return false;
+ }
+
+ var distance = this.eyePoint.distanceTo(extent.center),
+ pixelSize = this.pixelSizeAtDistance(distance);
+
+ return 2 * extent.radius < numPixels * pixelSize; // extent diameter less than size of num pixels
+};
+
+/**
+ * Returns the VBO ID of an array buffer containing a unit cube expressed as eight 3D vertices at (0, 1, 0),
+ * (0, 0, 0), (1, 1, 0), (1, 0, 0), (0, 1, 1), (0, 0, 1), (1, 1, 1) and (1, 0, 1). The buffer is created on
+ * first use and cached. Subsequent calls to this method return the cached buffer.
+ * @returns {Object} The VBO ID identifying the array buffer.
+ */
+DrawContext.prototype.unitCubeBuffer = function () {
+ var vboId = this.gpuResourceCache.resourceForKey(DrawContext.unitCubeKey);
+
+ if (!vboId) {
+ var gl = this.currentGlContext,
+ points = new Float32Array(24),
+ i = 0;
+
+ points[i++] = 0; // upper left corner, z = 0
+ points[i++] = 1;
+ points[i++] = 0;
+ points[i++] = 0; // lower left corner, z = 0
+ points[i++] = 0;
+ points[i++] = 0;
+ points[i++] = 1; // upper right corner, z = 0
+ points[i++] = 1;
+ points[i++] = 0;
+ points[i++] = 1; // lower right corner, z = 0
+ points[i++] = 0;
+ points[i++] = 0;
+
+ points[i++] = 0; // upper left corner, z = 1
+ points[i++] = 1;
+ points[i++] = 1;
+ points[i++] = 0; // lower left corner, z = 1
+ points[i++] = 0;
+ points[i++] = 1;
+ points[i++] = 1; // upper right corner, z = 1
+ points[i++] = 1;
+ points[i++] = 1;
+ points[i++] = 1; // lower right corner, z = 1
+ points[i++] = 0;
+ points[i] = 1;
+
+ vboId = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, vboId);
+ gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
+ this.frameStatistics.incrementVboLoadCount(1);
+
+ this.gpuResourceCache.putResource(DrawContext.unitCubeKey, vboId, points.length * 4);
+ }
+
+ return vboId;
+};
+
+/**
+ * Returns the VBO ID of a element array buffer containing the tessellation of a unit cube expressed as
+ * a single buffer containing both triangle indices and line indices. This is intended for use in conjunction
+ * with unitCubeBuffer. The unit cube's interior and outline may be rasterized as shown in the
+ * following WebGL pseudocode:
+ *
+ * The buffer is created on first use
+ * and cached. Subsequent calls to this method return the cached buffer.
+ * @returns {Object} The VBO ID identifying the element array buffer.
+ */
+DrawContext.prototype.unitCubeElements = function () {
+ var vboId = this.gpuResourceCache.resourceForKey(DrawContext.unitCubeElementsKey);
+
+ if (!vboId) {
+ var gl = this.currentGlContext,
+ elems = new Int16Array(60),
+ i = 0;
+
+ // interior
+
+ elems[i++] = 1; // -z face
+ elems[i++] = 0;
+ elems[i++] = 3;
+ elems[i++] = 3;
+ elems[i++] = 0;
+ elems[i++] = 2;
+
+ elems[i++] = 4; // +z face
+ elems[i++] = 5;
+ elems[i++] = 6;
+ elems[i++] = 6;
+ elems[i++] = 5;
+ elems[i++] = 7;
+
+ elems[i++] = 5; // -y face
+ elems[i++] = 1;
+ elems[i++] = 7;
+ elems[i++] = 7;
+ elems[i++] = 1;
+ elems[i++] = 3;
+
+ elems[i++] = 6; // +y face
+ elems[i++] = 2;
+ elems[i++] = 4;
+ elems[i++] = 4;
+ elems[i++] = 2;
+ elems[i++] = 0;
+
+ elems[i++] = 4; // -x face
+ elems[i++] = 0;
+ elems[i++] = 5;
+ elems[i++] = 5;
+ elems[i++] = 0;
+ elems[i++] = 1;
+
+ elems[i++] = 7; // +x face
+ elems[i++] = 3;
+ elems[i++] = 6;
+ elems[i++] = 6;
+ elems[i++] = 3;
+ elems[i++] = 2;
+
+ // outline
+
+ elems[i++] = 0; // left, -z
+ elems[i++] = 1;
+ elems[i++] = 1; // bottom, -z
+ elems[i++] = 3;
+ elems[i++] = 3; // right, -z
+ elems[i++] = 2;
+ elems[i++] = 2; // top, -z
+ elems[i++] = 0;
+
+ elems[i++] = 4; // left, +z
+ elems[i++] = 5;
+ elems[i++] = 5; // bottom, +z
+ elems[i++] = 7;
+ elems[i++] = 7; // right, +z
+ elems[i++] = 6;
+ elems[i++] = 6; // top, +z
+ elems[i++] = 4;
+
+ elems[i++] = 0; // upper left
+ elems[i++] = 4;
+ elems[i++] = 5; // lower left
+ elems[i++] = 1;
+ elems[i++] = 2; // upper right
+ elems[i++] = 6;
+ elems[i++] = 7; // lower right
+ elems[i] = 3;
+
+ vboId = gl.createBuffer();
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, vboId);
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, elems, gl.STATIC_DRAW);
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
+ this.frameStatistics.incrementVboLoadCount(1);
+
+ this.gpuResourceCache.putResource(DrawContext.unitCubeElementsKey, vboId, elems.length * 2);
+ }
+
+ return vboId;
+};
+
+/**
+ * Returns the VBO ID of a buffer containing a unit quadrilateral expressed as four 2D vertices at (0, 1),
+ * (0, 0), (1, 1) and (1, 0). The four vertices are in the order required by a triangle strip. The buffer is
+ * created on first use and cached. Subsequent calls to this method return the cached buffer.
+ * @returns {Object} The VBO ID identifying the vertex buffer.
+ */
+DrawContext.prototype.unitQuadBuffer = function () {
+ var vboId = this.gpuResourceCache.resourceForKey(DrawContext.unitQuadKey);
+
+ if (!vboId) {
+ var gl = this.currentGlContext,
+ points = new Float32Array(8);
+
+ points[0] = 0; // upper left corner
+ points[1] = 1;
+ points[2] = 0; // lower left corner
+ points[3] = 0;
+ points[4] = 1; // upper right corner
+ points[5] = 1;
+ points[6] = 1; // lower right corner
+ points[7] = 0;
+
+ vboId = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, vboId);
+ gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
+ this.frameStatistics.incrementVboLoadCount(1);
+
+ this.gpuResourceCache.putResource(DrawContext.unitQuadKey, vboId, points.length * 4);
+ }
+
+ return vboId;
+};
+
+/**
+ * Returns the VBO ID of a buffer containing a unit quadrilateral expressed as four 3D vertices at (0, 1, 0),
+ * (0, 0, 0), (1, 1, 0) and (1, 0, 0).
+ * The four vertices are in the order required by a triangle strip. The buffer is created
+ * on first use and cached. Subsequent calls to this method return the cached buffer.
+ * @returns {Object} The VBO ID identifying the vertex buffer.
+ */
+DrawContext.prototype.unitQuadBuffer3 = function () {
+ var vboId = this.gpuResourceCache.resourceForKey(DrawContext.unitQuadKey3);
+
+ if (!vboId) {
+ var gl = this.currentGlContext,
+ points = new Float32Array(12);
+
+ points[0] = 0; // upper left corner
+ points[1] = 1;
+ points[2] = 0;
+ points[3] = 0; // lower left corner
+ points[4] = 0;
+ points[5] = 0;
+ points[6] = 1; // upper right corner
+ points[7] = 1;
+ points[8] = 0;
+ points[9] = 1; // lower right corner
+ points[10] = 0;
+ points[11] = 0;
+
+ vboId = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, vboId);
+ gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
+ this.frameStatistics.incrementVboLoadCount(1);
+
+ this.gpuResourceCache.putResource(DrawContext.unitQuadKey3, vboId, points.length * 4);
+ }
+
+ return vboId;
+};
+
+/**
+ * Computes a Cartesian point at a location on the surface of this terrain according to a specified
+ * altitude mode. If there is no current terrain, this function approximates the returned point by assuming
+ * the terrain is the globe's ellipsoid.
+ * @param {Number} latitude The location's latitude.
+ * @param {Number} longitude The location's longitude.
+ * @param {Number} offset Distance above the terrain, in meters relative to the specified altitude mode, at
+ * which to compute the point.
+ * @param {String} altitudeMode The altitude mode to use to compute the point. Recognized values are
+ * WorldWind.ABSOLUTE, WorldWind.CLAMP_TO_GROUND and
+ * WorldWind.RELATIVE_TO_GROUND. The mode WorldWind.ABSOLUTE is used if the
+ * specified mode is null, undefined or unrecognized, or if the specified location is outside this terrain.
+ * @param {Vec3} result A pre-allocated Vec3 in which to return the computed point.
+ * @returns {Vec3} The specified result parameter, set to the coordinates of the computed point.
+ * @throws {ArgumentError} If the specified result argument is null or undefined.
+ */
+DrawContext.prototype.surfacePointForMode = function (latitude, longitude, offset, altitudeMode, result) {
+ if (!result) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "DrawContext", "surfacePointForMode", "missingResult"));
+ }
+
+ if (this.terrain) {
+ this.terrain.surfacePointForMode(latitude, longitude, offset, altitudeMode, result);
+ } else {
+ var h = offset + this.globe.elevationAtLocation(latitude, longitude) * this.verticalExaggeration;
+ this.globe.computePointFromPosition(latitude, longitude, h, result);
+ }
+
+ return result;
+};
+
+/**
+ * Transforms the specified model point from model coordinates to WebGL screen coordinates.
+ *
+ * // Assumes that the VBO returned by unitCubeBuffer is used as the source of vertex positions.
+ * bindBuffer(ELEMENT_ARRAY_BUFFER, drawContext.unitCubeElements());
+ * drawElements(TRIANGLES, 36, UNSIGNED_SHORT, 0); // draw the unit cube interior
+ * drawElements(LINES, 24, UNSIGNED_SHORT, 72); // draw the unit cube outline
+ *
+ * The resultant screen point is in WebGL screen coordinates, with the origin in the bottom-left corner and + * axes that extend up and to the right from the origin. + *
+ * This function stores the transformed point in the result argument, and returns true or false to indicate + * whether or not the transformation is successful. It returns false if the modelview or + * projection matrices are malformed, or if the specified model point is clipped by the near clipping plane or + * the far clipping plane. + * + * @param {Vec3} modelPoint The model coordinate point to project. + * @param {Vec3} result A pre-allocated vector in which to return the projected point. + * @returns {boolean} true if the transformation is successful, otherwise false. + * @throws {ArgumentError} If either the specified point or result argument is null or undefined. + */ +DrawContext.prototype.project = function (modelPoint, result) { + if (!modelPoint) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "DrawContext", "project", + "missingPoint")); + } + + if (!result) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "DrawContext", "project", + "missingResult")); + } + + // Transform the model point from model coordinates to eye coordinates then to clip coordinates. This + // inverts the Z axis and stores the negative of the eye coordinate Z value in the W coordinate. + var mx = modelPoint[0], + my = modelPoint[1], + mz = modelPoint[2], + m = this.modelviewProjection, + x = m[0] * mx + m[1] * my + m[2] * mz + m[3], + y = m[4] * mx + m[5] * my + m[6] * mz + m[7], + z = m[8] * mx + m[9] * my + m[10] * mz + m[11], + w = m[12] * mx + m[13] * my + m[14] * mz + m[15]; + + if (w === 0) { + return false; + } + + // Complete the conversion from model coordinates to clip coordinates by dividing by W. The resultant X, Y + // and Z coordinates are in the range [-1,1]. + x /= w; + y /= w; + z /= w; + + // Clip the point against the near and far clip planes. + if (z < -1 || z > 1) { + return false; + } + + // Convert the point from clip coordinate to the range [0,1]. This enables the X and Y coordinates to be + // converted to screen coordinates, and the Z coordinate to represent a depth value in the range[0,1]. + x = x * 0.5 + 0.5; + y = y * 0.5 + 0.5; + z = z * 0.5 + 0.5; + + // Convert the X and Y coordinates from the range [0,1] to screen coordinates. + x = x * this.viewport.width + this.viewport.x; + y = y * this.viewport.height + this.viewport.y; + + result[0] = x; + result[1] = y; + result[2] = z; + + return true; +}; + +/** + * Transforms the specified model point from model coordinates to WebGL screen coordinates, applying an offset + * to the modelPoint's projected depth value. + *
+ * The resultant screen point is in WebGL screen coordinates, with the origin in the bottom-left corner and axes + * that extend up and to the right from the origin. + *
+ * This function stores the transformed point in the result argument, and returns true or false to indicate whether or + * not the transformation is successful. It returns false if the modelview or projection + * matrices are malformed, or if the modelPoint is clipped by the near clipping plane or the far clipping plane, + * ignoring the depth offset. + *
+ * The depth offset may be any real number and is typically used to move the screenPoint slightly closer to the + * user's eye in order to give it visual priority over nearby objects or terrain. An offset of zero has no effect. + * An offset less than zero brings the screenPoint closer to the eye, while an offset greater than zero pushes the + * projected screen point away from the eye. + *
+ * Applying a non-zero depth offset has no effect on whether the model point is clipped by this method or by + * WebGL. Clipping is performed on the original model point, ignoring the depth offset. The final depth value + * after applying the offset is clamped to the range [0,1]. + * + * @param {Vec3} modelPoint The model coordinate point to project. + * @param {Number} depthOffset The amount of offset to apply. + * @param {Vec3} result A pre-allocated vector in which to return the projected point. + * @returns {boolean} true if the transformation is successful, otherwise false. + * @throws {ArgumentError} If either the specified point or result argument is null or undefined. + */ +DrawContext.prototype.projectWithDepth = function (modelPoint, depthOffset, result) { + if (!modelPoint) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "DrawContext", "projectWithDepth", + "missingPoint")); + } + + if (!result) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "DrawContext", "projectWithDepth", + "missingResult")); + } + + // Transform the model point from model coordinates to eye coordinates. The eye coordinate and the clip + // coordinate are transformed separately in order to reuse the eye coordinate below. + var mx = modelPoint[0], + my = modelPoint[1], + mz = modelPoint[2], + m = this.modelview, + ex = m[0] * mx + m[1] * my + m[2] * mz + m[3], + ey = m[4] * mx + m[5] * my + m[6] * mz + m[7], + ez = m[8] * mx + m[9] * my + m[10] * mz + m[11], + ew = m[12] * mx + m[13] * my + m[14] * mz + m[15]; + + // Transform the point from eye coordinates to clip coordinates. + var p = this.projection, + x = p[0] * ex + p[1] * ey + p[2] * ez + p[3] * ew, + y = p[4] * ex + p[5] * ey + p[6] * ez + p[7] * ew, + z = p[8] * ex + p[9] * ey + p[10] * ez + p[11] * ew, + w = p[12] * ex + p[13] * ey + p[14] * ez + p[15] * ew; + + if (w === 0) { + return false; + } + + // Complete the conversion from model coordinates to clip coordinates by dividing by W. The resultant X, Y + // and Z coordinates are in the range [-1,1]. + x /= w; + y /= w; + z /= w; + + // Clip the point against the near and far clip planes. + if (z < -1 || z > 1) { + return false; + } + + // Transform the Z eye coordinate to clip coordinates again, this time applying a depth offset. The depth + // offset is applied only to the matrix element affecting the projected Z coordinate, so we inline the + // computation here instead of re-computing X, Y, Z and W in order to improve performance. See + // Matrix.offsetProjectionDepth for more information on the effect of this offset. + z = p[8] * ex + p[9] * ey + p[10] * ez * (1 + depthOffset) + p[11] * ew; + z /= w; + + // Clamp the point to the near and far clip planes. We know the point's original Z value is contained within + // the clip planes, so we limit its offset z value to the range [-1, 1] in order to ensure it is not clipped + // by WebGL. In clip coordinates the near and far clip planes are perpendicular to the Z axis and are + // located at -1 and 1, respectively. + z = WWMath.clamp(z, -1, 1); + + // Convert the point from clip coordinates to the range [0, 1]. This enables the XY coordinates to be + // converted to screen coordinates, and the Z coordinate to represent a depth value in the range [0, 1]. + x = x * 0.5 + 0.5; + y = y * 0.5 + 0.5; + z = z * 0.5 + 0.5; + + // Convert the X and Y coordinates from the range [0,1] to screen coordinates. + x = x * this.viewport.width + this.viewport.x; + y = y * this.viewport.height + this.viewport.y; + + result[0] = x; + result[1] = y; + result[2] = z; + + return true; +}; + +/** + * Converts a window-coordinate point to WebGL screen coordinates. + *
+ * The specified point is understood to be in the window coordinate system of the WorldWindow, with the origin + * in the top-left corner and axes that extend down and to the right from the origin point. + *
+ * The returned point is in WebGL screen coordinates, with the origin in the bottom-left corner and axes that + * extend up and to the right from the origin point. + * + * @param {Vec2} point The window-coordinate point to convert. + * @param {Vec2} result A pre-allocated {@link Vec2} in which to return the computed point. + * @returns {Vec2} The specified result argument set to the computed point. + * @throws {ArgumentError} If either argument is null or undefined. + */ +DrawContext.prototype.convertPointToViewport = function (point, result) { + if (!point) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "DrawContext", "convertPointToViewport", + "missingPoint")); + } + + if (!result) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "DrawContext", "convertPointToViewport", + "missingResult")); + } + + result[0] = point[0]; + result[1] = this.viewport.height - point[1]; + + return result; +}; + +/** + * Computes the approximate size of a pixel at a specified distance from the eye point. + *
+ * This method assumes rectangular pixels, where pixel coordinates denote + * infinitely thin spaces between pixels. The units of the returned size are in model coordinates per pixel + * (usually meters per pixel). This returns 0 if the specified distance is zero. The returned size is undefined + * if the distance is less than zero. + * + * @param {Number} distance The distance from the eye point at which to determine pixel size, in model + * coordinates. + * @returns {Number} The approximate pixel size at the specified distance from the eye point, in model + * coordinates per pixel. + */ +DrawContext.prototype.pixelSizeAtDistance = function (distance) { + // Compute the pixel size from the width of a rectangle carved out of the frustum in model coordinates at + // the specified distance along the -Z axis and the viewport width in screen coordinates. The pixel size is + // expressed in model coordinates per screen coordinate (e.g. meters per pixel). + // + // The frustum width is determined by noticing that the frustum size is a linear function of distance from + // the eye point. The linear equation constants are determined during initialization, then solved for + // distance here. + // + // This considers only the frustum width by assuming that the frustum and viewport share the same aspect + // ratio, so that using either the frustum width or height results in the same pixel size. + + return this.pixelSizeFactor * distance + this.pixelSizeOffset; +}; + +/** + * Propagates the values contained in a TextAttributes object to the currently attached TextRenderer + * {@link TextRenderer} as to provide format to a string of text. It first checks if the 2D texture is not + * already cached according to the text string and its attached TextAttributes {@link TextAttributes} state key. + * The TextRenderer then produces a 2D Texture with the aforementioned text and format to be used as a label + * for a Text {@link Text} subclass (e.g. Annotation {@link Annotation} or Placemark {@link Placemark}). + * @param {String} text The string of text that will be given color, font, and outline + * from which the resulting texture will be based on. + * @param {TextAttributes} textAttributes Attributes that will be applied to the string. + * See TextAttributes {@link TextAttributes}. + * @returns {Texture} A texture {@link Texture} with the specified text string, font, colors, and outline. + */ +DrawContext.prototype.createTextTexture = function (text, textAttributes) { + if (!text || !textAttributes) { + return null; + } + + var textureKey = this.computeTextTextureStateKey(text, textAttributes); + var texture = this.gpuResourceCache.resourceForKey(textureKey); + + if (!texture) { + this.textRenderer.textColor = textAttributes.color; + this.textRenderer.typeFace = textAttributes.font; + this.textRenderer.enableOutline = textAttributes.enableOutline; + this.textRenderer.outlineColor = textAttributes.outlineColor; + this.textRenderer.outlineWidth = textAttributes.outlineWidth; + texture = this.textRenderer.renderText(text); + this.gpuResourceCache.putResource(textureKey, texture, texture.size); + this.gpuResourceCache.setResourceAgingFactor(textureKey, 100); // age this texture 100x faster than normal resources (e.g., tiles) + } + + return texture; +}; + +/** + * Computes a state key that relates to a text label, foregoing the TextAttributes {@link TextAttributes} + * properties that are not related to texture rendering (offset, scale, and depthTest). + * @param {String} text The label's string of text. + * @param {TextAttributes} attributes The TextAttributes object associated with the text label to render. + * @returns {String} A state key composed of the original string of text plus the TextAttributes associated + * with texture rendering. + */ +DrawContext.prototype.computeTextTextureStateKey = function (text, attributes) { + if (!text || !attributes) { + return null; + } + + return text + + "c " + attributes.color.toHexString(true) + + " f " + attributes.font.toString() + + " eo " + attributes.enableOutline + + " ow " + attributes.outlineWidth + + " oc " + attributes.outlineColor.toHexString(true); +}; + +/** + * Returns a WebGL extension and caches the result for subsequent calls. + * + * @param {String} extensionName The name of the WebGL extension. + * @returns {Object|null} A WebGL extension object, or null if the extension is not available. + * @throws {ArgumentError} If the argument is null or undefined. + */ +DrawContext.prototype.getExtension = function (extensionName) { + if (!extensionName) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "DrawContext", "getExtension", + "missingExtensionName")); + } + + if (!(extensionName in this.glExtensionsCache)) { + this.glExtensionsCache[extensionName] = this.currentGlContext.getExtension(extensionName) || null; + } + + return this.glExtensionsCache[extensionName]; +}; + +export default DrawContext; diff --git a/web/test/WebWorldWind/src/render/FramebufferTexture.js b/web/test/WebWorldWind/src/render/FramebufferTexture.js new file mode 100644 index 00000000..b99655c6 --- /dev/null +++ b/web/test/WebWorldWind/src/render/FramebufferTexture.js @@ -0,0 +1,147 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports FramebufferTexture + */ +import ArgumentError from '../error/ArgumentError'; +import Logger from '../util/Logger'; +import '../util/WWMath'; + + +/** + * Constructs a framebuffer texture with the specified dimensions and an optional depth buffer. Use the + * [DrawContext.bindFramebuffer]{@link DrawContext#bindFramebuffer} function to make the program current during rendering. + * + * @alias FramebufferTexture + * @constructor + * @classdesc Represents an off-screen WebGL framebuffer. The framebuffer has color buffer stored in a 32 + * bit RGBA texture, and has an optional depth buffer of at least 16 bits. Applications typically do not + * interact with this class. WebGL framebuffers are created by instances of this class and made current when the + * DrawContext.bindFramebuffer function is invoked. + * @param {WebGLRenderingContext} gl The current WebGL rendering context. + * @param {Number} width The width of the framebuffer, in pixels. + * @param {Number} height The height of the framebuffer, in pixels. + * @param {Boolean} depth true to configure the framebuffer with a depth buffer of at least 16 bits, false to + * disable depth buffering. + * @throws {ArgumentError} If the specified draw context is null or undefined, or if the width or height is less + * than zero. + */ +function FramebufferTexture(gl, width, height, depth) { + if (!gl) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "FramebufferTexture", "constructor", + "missingGlContext")); + } + + if (width < 0 || height < 0) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "FramebufferTexture", "constructor", + "The framebuffer width or height is less than zero.")); + } + + /** + * The width of this framebuffer, in pixels. + * @type {Number} + * @readonly + */ + this.width = width; + + /** + * The height of this framebuffer, in pixels. + * @type {Number} + * @readonly + */ + this.height = height; + + /** + * Indicates whether or not this framebuffer has a depth buffer. + * @type {Boolean} + * @readonly + */ + this.depth = depth; + + /** + * Indicates the size of this framebuffer's WebGL resources, in bytes. + * @type {Number} + * @readonly + */ + this.size = width * height * 4 + (depth ? width * height * 2 : 0); + + /** + * Indicates the WebGL framebuffer object object associated with this framebuffer texture. + * @type {WebGLFramebuffer} + * @readonly + */ + this.framebufferId = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebufferId); + + // Internal. Intentionally not documented. Configure this framebuffer's color buffer. + this.texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, this.texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, + gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, + gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, + gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, + gl.CLAMP_TO_EDGE); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, + gl.RGBA, gl.UNSIGNED_BYTE, null); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, this.texture, 0); + + // Internal. Intentionally not documented. Configure this framebuffer's optional depth buffer. + this.depthBuffer = null; + if (depth) { + this.depthBuffer = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, this.depthBuffer); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, + width, height); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, + gl.RENDERBUFFER, this.depthBuffer); + } + + var e = gl.checkFramebufferStatus(gl.FRAMEBUFFER); + if (e != gl.FRAMEBUFFER_COMPLETE) { + Logger.logMessage(Logger.LEVEL_WARNING, "FramebufferTexture", "constructor", + "Error creating framebuffer: " + e); + this.framebufferId = null; + this.texture = null; + this.depthBuffer = null; + } + + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.bindRenderbuffer(gl.RENDERBUFFER, null); + gl.bindTexture(gl.TEXTURE_2D, null); +} + +/** + * Binds this off-screen framebuffer's texture in the current WebGL graphics context. This texture contains + * color fragments resulting from WebGL operations executed when this framebuffer is bound by a call to + * [FramebufferTexture.bindFramebuffer]{@link FramebufferTexture#bindFramebuffer}. + * + * @param {DrawContext} dc The current draw context. + * @returns {Boolean} true if this framebuffer's texture was bound successfully, otherwise false. + */ +FramebufferTexture.prototype.bind = function (dc) { + if (this.texture) { + dc.currentGlContext.bindTexture(gl.TEXTURE_2D, this.texture); + } + + return !!this.texture; +}; + +export default FramebufferTexture; diff --git a/web/test/WebWorldWind/src/render/FramebufferTile.js b/web/test/WebWorldWind/src/render/FramebufferTile.js new file mode 100644 index 00000000..cbc59c3b --- /dev/null +++ b/web/test/WebWorldWind/src/render/FramebufferTile.js @@ -0,0 +1,128 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports FramebufferTile + */ +import ArgumentError from '../error/ArgumentError'; +import FramebufferTexture from '../render/FramebufferTexture'; +import Logger from '../util/Logger'; +import Matrix from '../geom/Matrix'; +import Rectangle from '../geom/Rectangle'; +import TextureTile from '../render/TextureTile'; + + +/** + * Constructs a framebuffer tile. + * @alias FramebufferTile + * @constructor + * @augments TextureTile + * @classdesc Represents a WebGL framebuffer applied to a portion of a globe's terrain. The framebuffer's width + * and height in pixels are equal to this tile's [tileWidth]{@link FramebufferTile#tileWidth} and + * [tileHeight]{@link FramebufferTile#tileHeight}, respectively. The framebuffer can be made active by calling + * [bindFramebuffer]{@link FramebufferTile#bindFramebuffer}. Color fragments written to this + * tile's framebuffer can then be drawn on the terrain surface using a + * [SurfaceTileRenderer]{@link SurfaceTileRenderer}. + *
+ * This class is meant to be used internally. Applications typically do not interact with this class. + * @param {Sector} sector The sector this tile covers. + * @param {Level} level The level this tile is associated with. + * @param {Number} row This tile's row in the associated level. + * @param {Number} column This tile's column in the associated level. + * @param {String} cacheKey A string uniquely identifying this tile relative to other tiles. + * @throws {ArgumentError} If the specified sector or level is null or undefined, the row or column arguments + * are less than zero, or the cache name is null, undefined or empty. + */ +function FramebufferTile(sector, level, row, column, cacheKey) { + if (!cacheKey || cacheKey.length < 1) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "FramebufferTile", "constructor", + "The specified cache name is null, undefined or zero length.")); + } + + TextureTile.call(this, sector, level, row, column); // args are checked in the superclass' constructor + + // Assign the cacheKey as the gpuCacheKey (inherited from TextureTile). + this.gpuCacheKey = cacheKey; + + // Internal. Intentionally not documented. + this.textureTransform = Matrix.fromIdentity().setToUnitYFlip(); + + // Internal. Intentionally not documented. + this.mustClear = true; +} + +FramebufferTile.prototype = Object.create(TextureTile.prototype); + +/** + * Causes this tile to clear any color fragments written to its off-screen framebuffer. + * @param dc The current draw context. + */ +FramebufferTile.prototype.clearFramebuffer = function (dc) { + this.mustClear = true; +}; + +/** + * Causes this tile's off-screen framebuffer as the current WebGL framebuffer. WebGL operations that affect the + * framebuffer now affect this tile's framebuffer, rather than the default WebGL framebuffer. + * Color fragments are written to this tile's WebGL texture, which can be made active by calling + * [SurfaceTile.bind]{@link SurfaceTile#bind}. + * + * @param {DrawContext} dc The current draw context. + * @returns {Boolean} true if the framebuffer was bound successfully, otherwise false. + */ +FramebufferTile.prototype.bindFramebuffer = function (dc) { + var framebuffer = dc.gpuResourceCache.resourceForKey(this.gpuCacheKey); + + if (!framebuffer) { + framebuffer = this.createFramebuffer(dc); + } + + dc.bindFramebuffer(framebuffer); + + if (this.mustClear) { + this.doClearFramebuffer(dc); + this.mustClear = false; + } + + return true; +}; + +// Internal. Intentionally not documented. +FramebufferTile.prototype.createFramebuffer = function (dc) { + var framebuffer = new FramebufferTexture(dc.currentGlContext, this.tileWidth, this.tileHeight, false); + dc.gpuResourceCache.putResource(this.gpuCacheKey, framebuffer, framebuffer.size); + + return framebuffer; +}; + +// Internal. Intentionally not documented. +FramebufferTile.prototype.doClearFramebuffer = function (dc) { + var gl = dc.currentGlContext; + gl.clearColor(0, 0, 0, 0); + gl.clear(gl.COLOR_BUFFER_BIT); +}; + +/** + * Applies the appropriate texture transform to display this tile's WebGL texture. + * @param {DrawContext} dc The current draw context. + * @param {Matrix} matrix The matrix to apply the transform to. + */ +FramebufferTile.prototype.applyInternalTransform = function (dc, matrix) { + matrix.multiplyMatrix(this.textureTransform); +}; + +export default FramebufferTile; diff --git a/web/test/WebWorldWind/src/render/FramebufferTileController.js b/web/test/WebWorldWind/src/render/FramebufferTileController.js new file mode 100644 index 00000000..7862d6ae --- /dev/null +++ b/web/test/WebWorldWind/src/render/FramebufferTileController.js @@ -0,0 +1,251 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports FramebufferTileController + */ +import ArgumentError from '../error/ArgumentError'; +import FramebufferTile from '../render/FramebufferTile'; +import LevelSet from '../util/LevelSet'; +import Location from '../geom/Location'; +import Logger from '../util/Logger'; +import MemoryCache from '../cache/MemoryCache'; +import Sector from '../geom/Sector'; +import Tile from '../util/Tile'; + + +/** + * Constructs a framebuffer tile controller. + * @alias FramebufferTileController + * @constructor + * @classdesc Provides access to a multi-resolution WebGL framebuffer arranged as adjacent tiles in a pyramid. + * WorldWind shapes use this class internally to draw on the terrain surface. Applications typically do not + * interact with this class. + */ +function FramebufferTileController() { + + /** + * The width in pixels of framebuffers associated with this controller's tiles. + * @type {Number} + * @readonly + */ + this.tileWidth = 256; + + /** + * The height in pixels of framebuffers associated with this controller's tiles. + * @type {Number} + * @readonly + */ + this.tileHeight = 256; + + /** + * Controls the level of detail switching for this controller. The next highest resolution level is + * used when an image's texel size is greater than this number of pixels. + * @type {Number} + * @default 1.75 + */ + this.detailControl = 1.75; + + // Internal. Intentionally not documented. + this.levels = new LevelSet(Sector.FULL_SPHERE, new Location(360, 360), 18, this.tileWidth, this.tileHeight); + + // Internal. Intentionally not documented. + this.topLevelTiles = []; + + // Internal. Intentionally not documented. + this.currentTiles = []; + + // Internal. Intentionally not documented. + this.currentTimestamp = null; + + // Internal. Intentionally not documented. + this.currentGlobeStateKey = null; + + // Internal. Intentionally not documented. + this.tileCache = new MemoryCache(500000, 400000); + + // Internal. Intentionally not documented. + this.key = "FramebufferTileController " + ++FramebufferTileController.keyPool; +} + +// Internal. Intentionally not documented. +FramebufferTileController.keyPool = 0; // source of unique ids + +/** + * Returns a set of multi-resolution [FramebufferTile]{@link FramebufferTile} instances appropriate for the + * current draw context that overlap a specified sector. + * @param {DrawContext} dc The current draw context. + * @param {Sector} sector The geographic region of interest. + * @returns {Array} The set of multi-resolution framebuffer tiles that overlap the sector. + * @throws {ArgumentError} If the specified sector is null. + */ +FramebufferTileController.prototype.selectTiles = function (dc, sector) { + if (!sector) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "FramebufferTileController", + "selectTiles", "missingSector")); + } + + // Assemble a set of global tiles appropriate for the draw context. + this.assembleTiles(dc); + + // Collect the tiles that overlap the specified sector and mark them as selected. + var tiles = []; + for (var i = 0, len = this.currentTiles.length; i < len; i++) { + var tile = this.currentTiles[i]; + if (tile.sector.overlaps(sector)) { + tile.selected = true; + tiles.push(tile); + } + } + + return tiles; +}; + +/** + * Draws this multi-resolution framebuffer on the terrain surface then clears the framebuffer. This has no + * effect if the framebuffer is unchanged since the last call to render. + * @param {DrawContext} dc The current draw context. + */ +FramebufferTileController.prototype.render = function (dc) { + // Exit immediately if there are no framebuffer tiles. This can happen when there ar eno surface shapes in + // the scene, for example. + if (this.currentTiles.length == 0) { + return; + } + + // Collect the tiles that have changed since the last call to render. + var tiles = []; + for (var i = 0, len = this.currentTiles.length; i < len; i++) { + var tile = this.currentTiles[i]; + if (tile.selected) { + tiles.push(tile); + } + } + + // Draw the changed tiles on the terrain surface. + dc.surfaceTileRenderer.renderTiles(dc, tiles, 1); + + // Clear the changed tile's WebGL framebuffers. + var gl = dc.currentGlContext, + framebuffer = dc.currentFramebuffer; + try { + gl.clearColor(0, 0, 0, 0); + for (i = 0, len = tiles.length; i < len; i++) { + tile = tiles[i]; + tile.selected = false; + tile.bindFramebuffer(dc); + gl.clear(gl.COLOR_BUFFER_BIT); + } + } finally { + dc.bindFramebuffer(framebuffer); + } +}; + +// Internal. Intentionally not documented. +FramebufferTileController.prototype.assembleTiles = function (dc) { + var timestamp = dc.timestamp, + globeStateKey = dc.globeStateKey; + + if (this.currentTimestamp != timestamp || + this.currentGlobeStateKey != globeStateKey) { + + this.doAssembleTiles(dc); + + this.currentTimestamp = timestamp; + this.currentGlobeStateKey = globeStateKey; + } +}; + +// Internal. Intentionally not documented. +FramebufferTileController.prototype.doAssembleTiles = function (dc) { + this.currentTiles = []; + + if (!dc.terrain) { + return; + } + + if (this.topLevelTiles.length == 0) { + this.createTopLevelTiles(); + } + + for (var i = 0, len = this.topLevelTiles.length; i < len; i++) { + var tile = this.topLevelTiles[i]; + tile.update(dc); + + if (this.isTileVisible(dc, tile)) { + this.addTileOrDescendants(dc, tile); + } + } +}; + +// Internal. Intentionally not documented. +FramebufferTileController.prototype.createTile = function (sector, level, row, column) { + var tileKey = this.key + " " + level.levelNumber + "." + row + "." + column; + return new FramebufferTile(sector, level, row, column, tileKey); +}; + +// Internal. Intentionally not documented. +FramebufferTileController.prototype.createTopLevelTiles = function () { + Tile.createTilesForLevel(this.levels.firstLevel(), this, this.topLevelTiles); +}; + +// Internal. Intentionally not documented. +FramebufferTileController.prototype.addTileOrDescendants = function (dc, tile) { + if (this.tileMeetsRenderingCriteria(dc, tile)) { + this.addTile(tile); + return; + } + + var subTiles = tile.subdivideToCache(tile.level.nextLevel(), this, this.tileCache); + for (var i = 0, len = subTiles.length; i < len; i++) { + var child = subTiles[i]; + child.update(dc); + + if (this.isTileVisible(dc, child)) { + this.addTileOrDescendants(dc, child); + } + } +}; + +// Internal. Intentionally not documented. +FramebufferTileController.prototype.addTile = function (tile) { + this.currentTiles.push(tile); +}; + +// Internal. Intentionally not documented. +FramebufferTileController.prototype.isTileVisible = function (dc, tile) { + if (dc.globe.projectionLimits && !tile.sector.overlaps(dc.globe.projectionLimits)) { + return false; + } + + if (dc.pickingMode) { + return tile.extent.intersectsFrustum(dc.pickFrustum); + } + + return tile.extent.intersectsFrustum(dc.frustumInModelCoordinates); +}; + +// Internal. Intentionally not documented. +FramebufferTileController.prototype.tileMeetsRenderingCriteria = function (dc, tile) { + var s = this.detailControl; + if (tile.sector.minLatitude >= 75 || tile.sector.maxLatitude <= -75) { + s *= 1.2; + } + + return tile.level.isLastLevel() || !tile.mustSubdivide(dc, s); +}; + +export default FramebufferTileController; diff --git a/web/test/WebWorldWind/src/render/ImageTile.js b/web/test/WebWorldWind/src/render/ImageTile.js new file mode 100644 index 00000000..b5587cdb --- /dev/null +++ b/web/test/WebWorldWind/src/render/ImageTile.js @@ -0,0 +1,137 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports ImageTile + */ +import ArgumentError from '../error/ArgumentError'; +import Logger from '../util/Logger'; +import TextureTile from '../render/TextureTile'; +import Tile from '../util/Tile'; + + +/** + * Constructs an image tile. + * @alias ImageTile + * @constructor + * @classdesc Represents an image applied to a portion of a globe's terrain. Applications typically do not + * interact with this class. + * @augments TextureTile + * @param {Sector} sector The sector this tile covers. + * @param {Level} level The level this tile is associated with. + * @param {Number} row This tile's row in the associated level. + * @param {Number} column This tile's column in the associated level. + * @param {String} imagePath The full path to the image. + * @throws {ArgumentError} If the specified sector or level is null or undefined, the row or column arguments + * are less than zero, or the specified image path is null, undefined or empty. + * + */ +function ImageTile(sector, level, row, column, imagePath) { + if (!imagePath || imagePath.length < 1) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "ImageTile", "constructor", + "The specified image path is null, undefined or zero length.")); + } + + TextureTile.call(this, sector, level, row, column); // args are checked in the superclass' constructor + + /** + * This tile's image path. + * @type {String} + */ + this.imagePath = imagePath; + + /** + * The tile whose texture to use when this tile's texture is not available. + * @type {Matrix} + */ + this.fallbackTile = null; + + // Assign imagePath to gpuCacheKey (inherited from TextureTile). + this.gpuCacheKey = imagePath; +} + +ImageTile.prototype = Object.create(TextureTile.prototype); + +/** + * Returns the size of the this tile in bytes. + * @returns {Number} The size of this tile in bytes, not including the associated texture size. + */ +ImageTile.prototype.size = function () { + return this.__proto__.__proto__.size.call(this) + this.imagePath.length + 8; +}; + +/** + * Causes this tile's texture to be active. Implements [SurfaceTile.bind]{@link SurfaceTile#bind}. + * @param {DrawContext} dc The current draw context. + * @returns {Boolean} true if the texture was bound successfully, otherwise false. + */ +ImageTile.prototype.bind = function (dc) { + // Attempt to bind in TextureTile first. + var isBound = this.__proto__.__proto__.bind.call(this, dc); + if (isBound) { + return true; + } + + if (this.fallbackTile) { + return this.fallbackTile.bind(dc); + } + + return false; +}; + +/** + * If this tile's fallback texture is used, applies the appropriate texture transform to a specified matrix. + * @param {DrawContext} dc The current draw context. + * @param {Matrix} matrix The matrix to apply the transform to. + */ +ImageTile.prototype.applyInternalTransform = function (dc, matrix) { + if (this.fallbackTile && !dc.gpuResourceCache.resourceForKey(this.imagePath)) { + // Must apply a texture transform to map the tile's sector into its fallback's image. + this.applyFallbackTransform(matrix); + } +}; + +// Intentionally not documented. +ImageTile.prototype.applyFallbackTransform = function (matrix) { + var deltaLevel = this.level.levelNumber - this.fallbackTile.level.levelNumber; + if (deltaLevel <= 0) + return; + + var fbTileDeltaLat = this.fallbackTile.sector.deltaLatitude(), + fbTileDeltaLon = this.fallbackTile.sector.deltaLongitude(), + sx = this.sector.deltaLongitude() / fbTileDeltaLon, + sy = this.sector.deltaLatitude() / fbTileDeltaLat, + tx = (this.sector.minLongitude - this.fallbackTile.sector.minLongitude) / fbTileDeltaLon, + ty = (this.sector.minLatitude - this.fallbackTile.sector.minLatitude) / fbTileDeltaLat; + + // Apply a transform to the matrix that maps texture coordinates for this tile to texture coordinates for the + // fallback tile. Rather than perform the full set of matrix operations, a single multiply is performed with the + // precomputed non-zero values: + // + // Matrix trans = Matrix.fromTranslation(tx, ty, 0); + // Matrix scale = Matrix.fromScale(sxy, sxy, 1); + // matrix.multiply(trans); + // matrix.multiply(scale); + + matrix.multiply( + sx, 0, 0, tx, + 0, sy, 0, ty, + 0, 0, 1, 0, + 0, 0, 0, 1); +}; + +export default ImageTile; diff --git a/web/test/WebWorldWind/src/render/OrderedRenderable.js b/web/test/WebWorldWind/src/render/OrderedRenderable.js new file mode 100644 index 00000000..44b60b3a --- /dev/null +++ b/web/test/WebWorldWind/src/render/OrderedRenderable.js @@ -0,0 +1,75 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports OrderedRenderable + */ +import Logger from '../util/Logger'; +import UnsupportedOperationError from '../error/UnsupportedOperationError'; + + +/** + * Applications must not call this constructor. It is an interface class and is not meant to be instantiated + * directly. + * @alias OrderedRenderable + * @constructor + * @classdesc Represents an ordered renderable. + * This is an interface class and is not meant to be instantiated directly. + */ +function OrderedRenderable() { + + /** + * This ordered renderable's display name. + * @type {String} + * @default Renderable + */ + this.displayName = "Renderable"; + + /** + * Indicates whether this ordered renderable is enabled. + * @type {Boolean} + * @default true + */ + this.enabled = true; + + /** + * This ordered renderable's distance from the eye point in meters. + * @type {Number} + * @default Number.MAX_VALUE + */ + this.eyeDistance = Number.MAX_VALUE; + + /** + * The time at which this ordered renderable was inserted into the ordered rendering list. + * @type {Number} + * @default 0 + */ + this.insertionTime = 0; + + throw new UnsupportedOperationError( + Logger.logMessage(Logger.LEVEL_SEVERE, "OrderedRenderable", "constructor", "abstractInvocation")); +} + +/** + * Renders this ordered renderable. + * @param {DrawContext} dc The current draw context. + */ +OrderedRenderable.prototype.renderOrdered = function (dc) { + throw new UnsupportedOperationError( + Logger.logMessage(Logger.LEVEL_SEVERE, "OrderedRenderable", "renderOrdered", "abstractInvocation")); +}; + +export default OrderedRenderable; diff --git a/web/test/WebWorldWind/src/render/Renderable.js b/web/test/WebWorldWind/src/render/Renderable.js new file mode 100644 index 00000000..c54e8ec8 --- /dev/null +++ b/web/test/WebWorldWind/src/render/Renderable.js @@ -0,0 +1,76 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports Renderable + */ +import Logger from '../util/Logger'; +import UnsupportedOperationError from '../error/UnsupportedOperationError'; + + +/** + * Constructs a base renderable. + * @alias Renderable + * @constructor + * @classdesc Represents a shape or other object that can be rendered. This is an abstract class and is not + * meant to be instantiated directly. + */ +function Renderable() { + + /** + * The display name of the renderable. + * @type {String} + * @default "Renderable" + */ + this.displayName = "Renderable"; + + /** + * Indicates whether to display this renderable. + * @type {Boolean} + * @default true + */ + this.enabled = true; + + /** + * Indicates the object to return as the userObject of this shape when picked. If null, + * then this shape is returned as the userObject. + * @type {Object} + * @default null + * @see [PickedObject.userObject]{@link PickedObject#userObject} + */ + this.pickDelegate = null; + + /** + * An application defined object associated with this renderable. A typical use case is to associate + * application defined data with a picked renderable. + * @type {Object} + * @default An empty object + */ + this.userProperties = {}; +} + +/** + * Render this renderable. Some shapes actually draw themselves during this call, others only add themselves + * to the draw context's ordered rendering list for subsequent drawing when their renderOrdered method is called. + * This method is intended to be called by layers such as {@link RenderableLayer} and not by applications. + * @param {DrawContext} dc The current draw context. + */ +Renderable.prototype.render = function (dc) { + throw new UnsupportedOperationError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Renderable", "render", "abstractInvocation")); +}; + +export default Renderable; diff --git a/web/test/WebWorldWind/src/render/ScreenCreditController.js b/web/test/WebWorldWind/src/render/ScreenCreditController.js new file mode 100644 index 00000000..fbd81227 --- /dev/null +++ b/web/test/WebWorldWind/src/render/ScreenCreditController.js @@ -0,0 +1,128 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports ScreenCreditController + */ +import ArgumentError from '../error/ArgumentError'; +import Color from '../util/Color'; +import Font from '../util/Font'; +import Layer from '../layer/Layer'; +import Logger from '../util/Logger'; +import Offset from '../util/Offset'; +import ScreenText from '../shapes/ScreenText'; + + +/** + * Constructs a screen credit controller. + * @alias ScreenCreditController + * @constructor + * @augments Layer + * @classdesc Collects and displays screen credits. + */ +function ScreenCreditController() { + Layer.call(this, "ScreenCreditController"); + + /** + * An {@link Offset} indicating where to place the attributions on the screen. + * @type {Offset} + * @default The lower left corner of the window with an 11px left margin and a 2px bottom margin. + */ + this.creditPlacement = new Offset(WorldWind.OFFSET_PIXELS, 11, WorldWind.OFFSET_PIXELS, 2); + + /** + * The amount of horizontal spacing between adjacent attributions. + * @type {number} + * @default An 11px margin between attributions. + */ + this.creditMargin = 11; + + // Apply 50% opacity to all shapes rendered by this layer. + this.opacity = 0.5; + + // Internal. Intentionally not documented. + this.credits = []; +} + +ScreenCreditController.prototype = Object.create(Layer.prototype); + +/** + * Clears all credits from this controller. + */ +ScreenCreditController.prototype.clear = function () { + this.credits = []; +}; + +/** + * Adds a credit to this controller. + * @param {String} creditString The text to display in the credits area. + * @param {Color} color The color with which to draw the string. + * @param {String} hyperlinkUrl Optional argument if screen credit is intended to work as a hyperlink. + * @throws {ArgumentError} If either the specified string or color is null or undefined. + */ +ScreenCreditController.prototype.addCredit = function (creditString, color, hyperlinkUrl) { + if (!creditString) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "ScreenCreditController", "addCredit", "missingText")); + } + + if (!color) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "ScreenCreditController", "addCredit", "missingColor")); + } + + // Verify if text credit is not already in controller, if it is, don't add it. + for (var i = 0, len = this.credits.length; i < len; i++) { + if (this.credits[i].text === creditString) { + return; + } + } + + var credit = new ScreenText(new Offset(WorldWind.OFFSET_PIXELS, 0, WorldWind.OFFSET_PIXELS, 0), creditString); + credit.attributes.font = new Font(10); + credit.attributes.color = color; + credit.attributes.enableOutline = false; + credit.attributes.offset = new Offset(WorldWind.OFFSET_FRACTION, 0, WorldWind.OFFSET_FRACTION, 0); + + // Append new user property to store URL for hyperlinking. + // (See BasicWorldWindowController.handleClickOrTap). + if (hyperlinkUrl) { + credit.userProperties.url = hyperlinkUrl; + } + + this.credits.push(credit); +}; + +// Internal use only. Intentionally not documented. +ScreenCreditController.prototype.doRender = function (dc) { + var point = this.creditPlacement.offsetForSize(dc.viewport.width, dc.viewport.height); + + for (var i = 0, len = this.credits.length; i < len; i++) { + // Place the credit text on screen and render it. + this.credits[i].screenOffset.x = point[0]; + this.credits[i].screenOffset.y = point[1]; + this.credits[i].render(dc); + + // Advance the screen position for the next credit. + dc.textRenderer.typeFace = this.credits[i].attributes.font; + dc.textRenderer.outlineWidth = this.credits[i].attributes.outlineWidth; + dc.textRenderer.enableOutline = this.credits[i].attributes.enableOutline; + point[0] += dc.textRenderer.textSize(this.credits[i].text)[0]; + point[0] += this.creditMargin; + } +}; + +export default ScreenCreditController; diff --git a/web/test/WebWorldWind/src/render/SurfaceRenderable.js b/web/test/WebWorldWind/src/render/SurfaceRenderable.js new file mode 100644 index 00000000..084a5502 --- /dev/null +++ b/web/test/WebWorldWind/src/render/SurfaceRenderable.js @@ -0,0 +1,61 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports SurfaceRenderable + */ +import Logger from '../util/Logger'; +import UnsupportedOperationError from '../error/UnsupportedOperationError'; + + +/** + * Applications must not call this constructor. It is an interface class and is not meant to be instantiated + * directly. + * @alias SurfaceRenderable + * @constructor + * @classdesc Represents a surface renderable. + * This is an interface class and is not meant to be instantiated directly. + */ +function SurfaceRenderable() { + + /** + * This surface renderable's display name. + * @type {String} + * @default Renderable + */ + this.displayName = "Renderable"; + + /** + * Indicates whether this surface renderable is enabled. + * @type {Boolean} + * @default true + */ + this.enabled = true; + + throw new UnsupportedOperationError( + Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceRenderable", "constructor", "abstractInvocation")); +} + +/** + * Renders this surface renderable. + * @param {DrawContext} dc The current draw context. + */ +SurfaceRenderable.prototype.renderSurface = function (dc) { + throw new UnsupportedOperationError( + Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceRenderable", "renderSurface", "abstractInvocation")); +}; + +export default SurfaceRenderable; diff --git a/web/test/WebWorldWind/src/render/SurfaceTile.js b/web/test/WebWorldWind/src/render/SurfaceTile.js new file mode 100644 index 00000000..fe8dc219 --- /dev/null +++ b/web/test/WebWorldWind/src/render/SurfaceTile.js @@ -0,0 +1,72 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports SurfaceTile + */ +import ArgumentError from '../error/ArgumentError'; +import Logger from '../util/Logger'; +import Matrix from '../geom/Matrix'; +import Sector from '../geom/Sector'; +import UnsupportedOperationError from '../error/UnsupportedOperationError'; + + +/** + * Constructs a surface tile for a specified sector. + * @alias SurfaceTile + * @constructor + * @classdesc Defines an abstract base class for imagery to be rendered on terrain. Applications typically + * do not interact with this class. + * @param {Sector} sector The sector of this surface tile. + * @throws {ArgumentError} If the specified sector is null or undefined. + */ +function SurfaceTile(sector) { + if (!sector) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceTile", "constructor", + "missingSector")); + } + + /** + * The sector spanned by this surface tile. + * @type {Sector} + */ + this.sector = sector; +} + +/** + * Causes this surface tile to be active, typically by binding the tile's texture in WebGL. + * Subclasses must override this function. + * @param {DrawContext} dc The current draw context. + * @returns {Boolean} true if the resource was successfully bound, otherwise false. + */ +SurfaceTile.prototype.bind = function (dc) { + throw new UnsupportedOperationError( + Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceTile", "bind", "abstractInvocation")); +}; + +/** + * Applies this surface tile's internal transform, typically a texture transform to align the associated + * resource with the terrain. + * Subclasses must override this function. + * @param {DrawContext} dc The current draw context. + * @param {Matrix} matrix The transform to apply. + */ +SurfaceTile.prototype.applyInternalTransform = function (dc, matrix) { + throw new UnsupportedOperationError( + Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceTile", "applyInternalTransform", "abstractInvocation")); +}; + +export default SurfaceTile; diff --git a/web/test/WebWorldWind/src/render/SurfaceTileRenderer.js b/web/test/WebWorldWind/src/render/SurfaceTileRenderer.js new file mode 100644 index 00000000..434736b5 --- /dev/null +++ b/web/test/WebWorldWind/src/render/SurfaceTileRenderer.js @@ -0,0 +1,189 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports SurfaceTileRenderer + */ +import ArgumentError from '../error/ArgumentError'; +import Logger from '../util/Logger'; +import Matrix from '../geom/Matrix'; +import SurfaceShapeTile from '../shapes/SurfaceShapeTile'; +import SurfaceTileRendererProgram from '../shaders/SurfaceTileRendererProgram'; + + +/** + * Constructs a new surface tile renderer. + * @alias SurfaceTileRenderer + * @constructor + * @classdesc This class is responsible for rendering imagery onto the terrain. + * It is meant to be used internally. Applications typically do not interact with this class. + */ +function SurfaceTileRenderer() { + + // Scratch values to avoid constantly recreating these matrices. + this.texMaskMatrix = Matrix.fromIdentity(); + this.texSamplerMatrix = Matrix.fromIdentity(); + + // Internal. Intentionally not documented. + this.isSurfaceShapeTileRendering = false; +} + +/** + * Render a specified collection of surface tiles. + * @param {DrawContext} dc The current draw context. + * @param {SurfaceTile[]} surfaceTiles The surface tiles to render. + * @param {Number} opacity The opacity at which to draw the surface tiles. + * @param {Boolean} tilesHaveOpacity If true, incoming tiles each have their own opacity property and + * it's value is applied when the tile is drawn. + * @throws {ArgumentError} If the specified surface tiles array is null or undefined. + */ +SurfaceTileRenderer.prototype.renderTiles = function (dc, surfaceTiles, opacity, tilesHaveOpacity) { + if (!surfaceTiles) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceTileRenderer", "renderTiles", + "Specified surface tiles array is null or undefined.")); + } + + if (surfaceTiles.length < 1) + return; + + var terrain = dc.terrain, + gl = dc.currentGlContext, + tileCount = 0,// for frame statistics, + program, + terrainTile, + terrainTileSector, + surfaceTile, + currentTileOpacity = 1; + + if (!terrain) + return; + + this.isSurfaceShapeTileRendering = surfaceTiles[0] instanceof SurfaceShapeTile; + + opacity *= dc.surfaceOpacity; + + // For each terrain tile, render it for each overlapping surface tile. + program = this.beginRendering(dc, opacity); + terrain.beginRendering(dc); + try { + for (var i = 0, ttLen = terrain.surfaceGeometry.length; i < ttLen; i++) { + terrainTile = terrain.surfaceGeometry[i]; + terrainTileSector = terrainTile.sector; + + terrain.beginRenderingTile(dc, terrainTile); + try { + // Render the terrain tile for each overlapping surface tile. + for (var j = 0, stLen = surfaceTiles.length; j < stLen; j++) { + surfaceTile = surfaceTiles[j]; + if (surfaceTile.sector.overlaps(terrainTileSector)) { + if (surfaceTile.bind(dc)) { + if (dc.pickingMode) { + if (surfaceTile.pickColor) { + program.loadColor(gl, surfaceTile.pickColor); + } else { + // Surface shape tiles don't use a pick color. Pick colors are encoded into + // the colors of the individual shapes drawn into the tile. + } + } else { + if (tilesHaveOpacity && surfaceTile.opacity != currentTileOpacity) { + program.loadOpacity(gl, opacity * surfaceTile.opacity); + currentTileOpacity = surfaceTile.opacity; + } + } + + this.applyTileState(dc, terrainTile, surfaceTile); + terrain.renderTile(dc, terrainTile); + ++tileCount; + } + } + } + } + catch (e) { + console.log(e); + } + finally { + terrain.endRenderingTile(dc, terrainTile); + } + } + } + catch (e) { + console.log(e); + } + finally { + terrain.endRendering(dc); + this.endRendering(dc); + dc.frameStatistics.incrementRenderedTileCount(tileCount); + } +}; + +// Intentionally not documented. +SurfaceTileRenderer.prototype.beginRendering = function (dc, opacity) { + var gl = dc.currentGlContext, + program = dc.findAndBindProgram(SurfaceTileRendererProgram); + + program.loadTexSampler(gl, gl.TEXTURE0); + + if (dc.pickingMode && !this.isSurfaceShapeTileRendering) { + program.loadModulateColor(gl, true); + } else { + program.loadModulateColor(gl, false); + program.loadOpacity(gl, opacity); + } + + return program; +}; + +// Intentionally not documented. +SurfaceTileRenderer.prototype.endRendering = function (dc) { + var gl = dc.currentGlContext; + gl.bindTexture(gl.TEXTURE_2D, null); +}; + +// Intentionally not documented. +SurfaceTileRenderer.prototype.applyTileState = function (dc, terrainTile, surfaceTile) { + // Sets up the texture transform and mask that applies the texture tile to the terrain tile. + var gl = dc.currentGlContext, + program = dc.currentProgram, + terrainSector = terrainTile.sector, + terrainDeltaLat = terrainSector.deltaLatitude(), + terrainDeltaLon = terrainSector.deltaLongitude(), + surfaceSector = surfaceTile.sector, + rawSurfaceDeltaLat = surfaceSector.deltaLatitude(), + rawSurfaceDeltaLon = surfaceSector.deltaLongitude(), + surfaceDeltaLat = rawSurfaceDeltaLat > 0 ? rawSurfaceDeltaLat : 1, + surfaceDeltaLon = rawSurfaceDeltaLon > 0 ? rawSurfaceDeltaLon : 1, + sScale = terrainDeltaLon / surfaceDeltaLon, + tScale = terrainDeltaLat / surfaceDeltaLat, + sTrans = -(surfaceSector.minLongitude - terrainSector.minLongitude) / surfaceDeltaLon, + tTrans = -(surfaceSector.minLatitude - terrainSector.minLatitude) / surfaceDeltaLat; + + this.texMaskMatrix.set( + sScale, 0, 0, sTrans, + 0, tScale, 0, tTrans, + 0, 0, 1, 0, + 0, 0, 0, 1 + ); + + this.texSamplerMatrix.setToUnitYFlip(); + surfaceTile.applyInternalTransform(dc, this.texSamplerMatrix); + this.texSamplerMatrix.multiplyMatrix(this.texMaskMatrix); + + program.loadTexSamplerMatrix(gl, this.texSamplerMatrix); + program.loadTexMaskMatrix(gl, this.texMaskMatrix); +}; + +export default SurfaceTileRenderer; diff --git a/web/test/WebWorldWind/src/render/TextRenderer.js b/web/test/WebWorldWind/src/render/TextRenderer.js new file mode 100644 index 00000000..176c0182 --- /dev/null +++ b/web/test/WebWorldWind/src/render/TextRenderer.js @@ -0,0 +1,307 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports TextRenderer + */ +import ArgumentError from '../error/ArgumentError'; +import BasicTextureProgram from '../shaders/BasicTextureProgram'; +import Color from '../util/Color'; +import Font from '../util/Font'; +import Logger from '../util/Logger'; +import Matrix from '../geom/Matrix'; +import Texture from '../render/Texture'; +import Vec2 from '../geom/Vec2'; + + +/** + * Constructs a TextRenderer instance. + * @alias TextRenderer + * @constructor + * @classdesc Provides methods useful for displaying text. An instance of this class is attached to the + * WorldWindow {@link DrawContext} and is not intended to be used independently of that. Applications typically do + * not create instances of this class. + * @param {drawContext} drawContext The current draw context. Typically the same draw context that TextRenderer + * is attached to. + * @throws {ArgumentError} If the specified draw context is null or undefined. + */ +function TextRenderer(drawContext) { + if (!drawContext) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "TextRenderer", "constructor", + "missingDc")); + } + + // Internal use only. Intentionally not documented. + this.canvas2D = document.createElement("canvas"); + + // Internal use only. Intentionally not documented. + this.ctx2D = this.canvas2D.getContext("2d"); + + // Internal use only. Intentionally not documented. + this.dc = drawContext; + + /** + * Indicates if the text will feature an outline around its characters. + * @type {boolean} + */ + this.enableOutline = true; + + // Internal use only. Intentionally not documented. + this.lineSpacing = 0.15; // fraction of font size + + /** + * The color for the Text outline. + * Its default has half transparency to avoid visual artifacts that appear while fully opaque. + * @type {Color} + */ + this.outlineColor = new Color(0, 0, 0, 0.5); + + /** + * Indicates the text outline width (or thickness) in pixels. + * @type {number} + */ + this.outlineWidth = 4; + + /** + * The text color. + * @type {Color} + */ + this.textColor = new Color(1, 1, 1, 1); + + /** + * The text size, face and other characteristics, as described in [Font]{@link Font}. + * @type {Font} + */ + this.typeFace = new Font(14); +} + +/** + * Returns the width and height of a specified text string considering the current typeFace and outline usage. + * @param {string} text The text string. + * @returns {Vec2} A vector indicating the text's width and height, respectively, in pixels. + */ +TextRenderer.prototype.textSize = function (text) { + if (text.length === 0) { + return new Vec2(0, 0); + } + + this.ctx2D.font = this.typeFace.fontString; + + var lines = text.split("\n"), + height = lines.length * (this.typeFace.size * (1 + this.lineSpacing)), + maxWidth = 0; + + for (var i = 0; i < lines.length; i++) { + maxWidth = Math.max(maxWidth, this.ctx2D.measureText(lines[i]).width); + } + + if (this.enableOutline) { + maxWidth += this.outlineWidth; + height += this.outlineWidth; + } + + return new Vec2(maxWidth, height); +}; + +/** + * Creates a texture for a specified text string and current TextRenderer state. + * @param {String} text The text string. + * @returns {Texture} A texture for the specified text string. + */ +TextRenderer.prototype.renderText = function (text) { + if (text && text.length > 0) { + var canvas2D = this.drawText(text); + return new Texture(this.dc.currentGlContext, canvas2D); + } else { + return null; + } +}; + +/** + * Creates a 2D Canvas for a specified text string while considering current TextRenderer state in + * regards to outline usage and color, text color, typeface, and outline width. + * @param {String} text The text string. + * @returns {canvas2D} A 2D Canvas for the specified text string. + */ +TextRenderer.prototype.drawText = function (text) { + var ctx2D = this.ctx2D, + canvas2D = this.canvas2D, + textSize = this.textSize(text), + lines = text.split("\n"), + strokeOffset = this.enableOutline ? this.outlineWidth / 2 : 0, + pixelScale = this.dc.pixelScale; + + canvas2D.width = Math.ceil(textSize[0]) * pixelScale; + canvas2D.height = Math.ceil(textSize[1]) * pixelScale; + + ctx2D.scale(pixelScale, pixelScale); + ctx2D.font = this.typeFace.fontString; + ctx2D.textBaseline = "bottom"; + ctx2D.textAlign = this.typeFace.horizontalAlignment; + ctx2D.fillStyle = this.textColor.toCssColorString(); + ctx2D.strokeStyle = this.outlineColor.toCssColorString(); + ctx2D.lineWidth = this.outlineWidth; + ctx2D.lineCap = "round"; + ctx2D.lineJoin = "round"; + + if (this.typeFace.horizontalAlignment === "left") { + ctx2D.translate(strokeOffset, 0); + } else if (this.typeFace.horizontalAlignment === "right") { + ctx2D.translate(textSize[0] - strokeOffset, 0); + } else { + ctx2D.translate(textSize[0] / 2, 0); + } + + for (var i = 0; i < lines.length; i++) { + ctx2D.translate(0, this.typeFace.size * (1 + this.lineSpacing) + strokeOffset); + if (this.enableOutline) { + ctx2D.strokeText(lines[i], 0, 0); + } + ctx2D.fillText(lines[i], 0, 0); + } + + return canvas2D; +}; + +/** + * Calculates maximum line height based on the current typeFace and outline usage of TextRenderer. + * @returns {Vec2} A vector indicating the text's width and height, respectively, in pixels. + */ +TextRenderer.prototype.getMaxLineHeight = function () { + // Check underscore + capital E with acute accent + return this.textSize("_\u00c9")[1]; +}; + +/** + * Wraps the text based on width and height using new line delimiter + * @param {String} text The text to wrap. + * @param {Number} width The width in pixels. + * @param {Number} height The height in pixels. + * @returns {String} The wrapped text. + */ +TextRenderer.prototype.wrap = function (text, width, height) { + if (!text) { + throw new ArgumentError( + Logger.logMessage(Logger.WARNING, "TextRenderer", "wrap", "missing text")); + } + + var i; + + var lines = text.split("\n"); + var wrappedText = ""; + + // Wrap each line + for (i = 0; i < lines.length; i++) { + lines[i] = this.wrapLine(lines[i], width); + } + // Concatenate all lines in one string with new line separators + // between lines - not at the end + // Checks for height limit. + var currentHeight = 0; + var heightExceeded = false; + var maxLineHeight = this.getMaxLineHeight(); + for (i = 0; i < lines.length && !heightExceeded; i++) { + var subLines = lines[i].split("\n"); + for (var j = 0; j < subLines.length && !heightExceeded; j++) { + if (height <= 0 || currentHeight + maxLineHeight <= height) { + wrappedText += subLines[j]; + currentHeight += maxLineHeight + this.lineSpacing; + if (j < subLines.length - 1) { + wrappedText += '\n'; + } + } + else { + heightExceeded = true; + } + } + + if (i < lines.length - 1 && !heightExceeded) { + wrappedText += '\n'; + } + } + // Add continuation string if text truncated + if (heightExceeded) { + if (wrappedText.length > 0) { + wrappedText = wrappedText.substring(0, wrappedText.length - 1); + } + + wrappedText += "..."; + } + + return wrappedText; +}; + +/** + * Wraps a line of text based on width and height + * @param {String} text The text to wrap. + * @param {Number} width The width in pixels. + * @returns {String} The wrapped text. + */ +TextRenderer.prototype.wrapLine = function (text, width) { + var wrappedText = ""; + + // Single line - trim leading and trailing spaces + var source = text.trim(); + var lineBounds = this.textSize(source); + if (lineBounds[0] > width) { + // Split single line to fit preferred width + var line = ""; + var start = 0; + var end = source.indexOf(' ', start + 1); + while (start < source.length) { + if (end === -1) { + end = source.length; // last word + } + + // Extract a 'word' which is in fact a space and a word + var word = source.substring(start, end); + var linePlusWord = line + word; + if (this.textSize(linePlusWord)[0] <= width) { + // Keep adding to the current line + line += word; + } + else { + // Width exceeded + if (line.length !== 0) { + // Finish current line and start new one + wrappedText += line; + wrappedText += '\n'; + line = ""; + line += word.trim(); // get read of leading space(s) + } + else { + // Line is empty, force at least one word + line += word.trim(); + } + } + // Move forward in source string + start = end; + if (start < source.length - 1) { + end = source.indexOf(' ', start + 1); + } + } + // Gather last line + wrappedText += line; + } + else { + // Line doesn't need to be wrapped + wrappedText += source; + } + + return wrappedText; +}; + +export default TextRenderer; diff --git a/web/test/WebWorldWind/src/render/Texture.js b/web/test/WebWorldWind/src/render/Texture.js new file mode 100644 index 00000000..37ddeca2 --- /dev/null +++ b/web/test/WebWorldWind/src/render/Texture.js @@ -0,0 +1,187 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports Texture + */ +import ArgumentError from '../error/ArgumentError'; +import Logger from '../util/Logger'; +import WWMath from '../util/WWMath'; + + +/** + * Constructs a texture for a specified image. + * @alias Texture + * @constructor + * @classdesc Represents a WebGL texture. Applications typically do not interact with this class. + * @param {WebGLRenderingContext} gl The current WebGL rendering context. + * @param {Image} image The texture's image. + * @param {GLenum} wrapMode Optional. Specifies the wrap mode of the texture. Defaults to gl.CLAMP_TO_EDGE + * @throws {ArgumentError} If the specified WebGL context or image is null or undefined. + */ +function Texture(gl, image, wrapMode) { + + if (!gl) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Texture", "constructor", + "missingGlContext")); + } + + if (!image) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Texture", "constructor", + "missingImage")); + } + + if (!wrapMode) { + wrapMode = gl.CLAMP_TO_EDGE; + } + + var textureId = gl.createTexture(), + isPowerOfTwo = WWMath.isPowerOfTwo(image.width) && WWMath.isPowerOfTwo(image.height); + + this.originalImageWidth = image.width; + this.originalImageHeight = image.height; + + if (wrapMode === gl.REPEAT && !isPowerOfTwo) { + image = this.resizeImage(image); + isPowerOfTwo = true; + } + + this.imageWidth = image.width; + this.imageHeight = image.height; + this.size = image.width * image.height * 4; + + gl.bindTexture(gl.TEXTURE_2D, textureId); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, + isPowerOfTwo ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR); + + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapMode); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapMode); + + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1); + gl.texImage2D(gl.TEXTURE_2D, 0, + gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0); + + if (isPowerOfTwo) { + gl.generateMipmap(gl.TEXTURE_2D); + } + + this.textureId = textureId; + + /** + * The time at which this texture was created. + * @type {Date} + */ + this.creationTime = new Date(); + + // Internal use only. Intentionally not documented. + this.texParameters = {}; + + // Internal use only. Intentionally not documented. + // https://www.khronos.org/registry/webgl/extensions/EXT_texture_filter_anisotrop + this.anisotropicFilterExt = gl.getExtension("EXT_texture_filter_anisotropic") || + gl.getExtension("WEBKIT_EXT_texture_filter_anisotropic"); +} + +/** + * Sets a texture parameter to apply when binding this texture. + * + * Currently only gl.TEXTURE_MAG_FILTER has an effect. + * + * @param {Glenum} name The name of the parameter + * @param {GLint} value The value for this parameter + */ +Texture.prototype.setTexParameter = function (name, value) { + this.texParameters[name] = value; +}; + +/** + * Returns the value of a texture parameter to be assigned to this texture. + * @param {Glenum} name The name of the parameter + * @returns {GLint} The value for this parameter + */ +Texture.prototype.getTexParameter = function (name) { + return this.texParameters[name]; +}; + +/** + * Clears the list of texture parameters to apply when binding this texture. + */ +Texture.prototype.clearTexParameters = function () { + this.texParameters = {}; +}; + +/** + * Disposes of the WebGL texture object associated with this texture. + * @param gl + */ +Texture.prototype.dispose = function (gl) { + gl.deleteTexture(this.textureId); + delete this.textureId; +}; + +/** + * Binds this texture in the current WebGL graphics context. + * @param {DrawContext} dc The current draw context. + */ +Texture.prototype.bind = function (dc) { + var gl = dc.currentGlContext; + + gl.bindTexture(gl.TEXTURE_2D, this.textureId); + + this.applyTexParameters(dc); + + dc.frameStatistics.incrementTextureLoadCount(1); + return true; +}; + +/** + * Applies the configured texture parameters to the OpenGL context. + * @param {DrawContext} dc The current draw context. + */ +Texture.prototype.applyTexParameters = function (dc) { + var gl = dc.currentGlContext; + + // Configure the OpenGL texture magnification function. Use linear by default. + var textureMagFilter = this.texParameters[gl.TEXTURE_MAG_FILTER] || gl.LINEAR; + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, textureMagFilter); + + // Try to enable the anisotropic texture filtering only if we have a linear magnification filter. + // This can't be enabled all the time because Windows seems to ignore the TEXTURE_MAG_FILTER parameter when + // this extension is enabled. + if (textureMagFilter === gl.LINEAR) { + // Setup 4x anisotropic texture filtering when this feature is available. + if (this.anisotropicFilterExt) { + gl.texParameteri(gl.TEXTURE_2D, this.anisotropicFilterExt.TEXTURE_MAX_ANISOTROPY_EXT, 4); + } + } +}; + +/** + * Resizes an image to a power of two. + * @param {Image} image The image to resize. + */ +Texture.prototype.resizeImage = function (image) { + var canvas = document.createElement("canvas"); + canvas.width = WWMath.powerOfTwoFloor(image.width); + canvas.height = WWMath.powerOfTwoFloor(image.height); + var ctx = canvas.getContext("2d"); + ctx.drawImage(image, 0, 0, canvas.width, canvas.height); + return canvas; +}; + + +export default Texture; diff --git a/web/test/WebWorldWind/src/render/TextureTile.js b/web/test/WebWorldWind/src/render/TextureTile.js new file mode 100644 index 00000000..42337867 --- /dev/null +++ b/web/test/WebWorldWind/src/render/TextureTile.js @@ -0,0 +1,84 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports TextureTile + */ +import ArgumentError from '../error/ArgumentError'; +import Logger from '../util/Logger'; +import Tile from '../util/Tile'; + + +/** + * Constructs a texture tile. + * @alias TextureTile + * @constructor + * @augments Tile + * @classdesc Represents an image applied to a portion of a globe's terrain. Applications typically do not + * interact with this class. + * @param {Sector} sector The sector this tile covers. + * @param {Level} level The level this tile is associated with. + * @param {Number} row This tile's row in the associated level. + * @param {Number} column This tile's column in the associated level. + * @throws {ArgumentError} If the specified sector or level is null or undefined, the row or column arguments + * are less than zero, or the specified image path is null, undefined or empty. + * + */ +function TextureTile(sector, level, row, column) { + Tile.call(this, sector, level, row, column); // args are checked in the superclass' constructor + + /** + * GPU cache key + * @type {string} + */ + this.gpuCacheKey = null; +} + +TextureTile.prototype = Object.create(Tile.prototype); + +/** + * Returns the size of the this tile in bytes. + * @returns {Number} The size of this tile in bytes, not including the associated texture size. + */ +TextureTile.prototype.size = function () { + return Tile.prototype.size.call(this); +}; + +/** + * Causes this tile's texture to be active. Implements [SurfaceTile.bind]{@link SurfaceTile#bind}. + * @param {DrawContext} dc The current draw context. + * @returns {Boolean} true if the texture was bound successfully, otherwise false. + */ +TextureTile.prototype.bind = function (dc) { + var texture = dc.gpuResourceCache.resourceForKey(this.gpuCacheKey); + if (texture) { + return texture.bind(dc); + } + + return false; +}; + +/** + * If this tile's fallback texture is used, applies the appropriate texture transform to a specified matrix. + * Otherwise, this is a no-op. + * @param {DrawContext} dc The current draw context. + * @param {Matrix} matrix The matrix to apply the transform to. + */ +TextureTile.prototype.applyInternalTransform = function (dc, matrix) { + // Override this method if the tile has a fallback texture. +}; + +export default TextureTile; diff --git a/web/test/WebWorldWind/src/shaders/AtmosphereProgram.js b/web/test/WebWorldWind/src/shaders/AtmosphereProgram.js new file mode 100644 index 00000000..080435d7 --- /dev/null +++ b/web/test/WebWorldWind/src/shaders/AtmosphereProgram.js @@ -0,0 +1,320 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports AtmosphereProgram + */ +import ArgumentError from '../error/ArgumentError'; +import GpuProgram from '../shaders/GpuProgram'; +import Logger from '../util/Logger'; + + +/** + * Constructs a new program. + * Initializes, compiles and links this GLSL program with the source code for its vertex and fragment shaders. + *
+ * This method creates WebGL shaders for the program's shader sources and attaches them to a new GLSL program. + * This method then compiles the shaders and then links the program if compilation is successful. + * + * @alias AtmosphereProgram + * @constructor + * @augments GpuProgram + * @classdesc AtmosphereProgram is a GLSL program that draws the atmosphere. + * @param {WebGLRenderingContext} gl The current WebGL context. + * @throws {ArgumentError} If the shaders cannot be compiled, or linking of + * the compiled shaders into a program fails. + */ +function AtmosphereProgram(gl, vertexShaderSource, fragmentShaderSource, attribute) { + + // Call to the superclass, which performs shader program compiling and linking. + GpuProgram.call(this, gl, vertexShaderSource, fragmentShaderSource, attribute); + + + // Frag color mode indicates the atmospheric scattering color components written to the fragment color. + this.FRAGMODE_SKY = 1; + this.FRAGMODE_GROUND_PRIMARY = 2; + this.FRAGMODE_GROUND_SECONDARY = 3; + this.FRAGMODE_GROUND_PRIMARY_TEX_BLEND = 4; + + /** + * The globe's atmosphere altitude. + * @type {Number} + * @default 160000.0 meters + */ + this.altitude = 160000; + + /** + * This atmosphere's Rayleigh scale depth. + * @type {Number} + * @default 0.25 + */ + this.rayleighScaleDepth = 0.25; + + /** + * The WebGL location for this program's 'fragMode' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.fragModeLocation = this.uniformLocation(gl, "fragMode"); + + /** + * The WebGL location for this program's 'mvpMatrix' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.mvpMatrixLocation = this.uniformLocation(gl, "mvpMatrix"); + + /** + * The WebGL location for this program's 'texCoordMatrix' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.texCoordMatrixLocation = this.uniformLocation(gl, "texCoordMatrix"); + + /** + * The WebGL location for this program's 'vertexOrigin' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.vertexOriginLocation = this.uniformLocation(gl, "vertexOrigin"); + + /** + * The WebGL location for this program's 'eyePoint' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.eyePointLocation = this.uniformLocation(gl, "eyePoint"); + + /** + * The WebGL location for this program's 'eyeMagnitude' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.eyeMagnitudeLocation = this.uniformLocation(gl, "eyeMagnitude"); + + /** + * The WebGL location for this program's 'eyeMagnitude2' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.eyeMagnitude2Location = this.uniformLocation(gl, "eyeMagnitude2"); + + /** + * The WebGL location for this program's 'lightDirection' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.lightDirectionLocation = this.uniformLocation(gl, "lightDirection"); + + /** + * The WebGL location for this program's 'atmosphereRadius' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.atmosphereRadiusLocation = this.uniformLocation(gl, "atmosphereRadius"); + + /** + * The WebGL location for this program's 'atmosphereRadius2' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.atmosphereRadius2Location = this.uniformLocation(gl, "atmosphereRadius2"); + + /** + * The WebGL location for this program's 'globeRadius' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.globeRadiusLocation = this.uniformLocation(gl, "globeRadius"); + + /** + * The WebGL location for this program's 'scale' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.scaleLocation = this.uniformLocation(gl, "scale"); + + /** + * The WebGL location for this program's 'scaleDepth' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.scaleDepthLocation = this.uniformLocation(gl, "scaleDepth"); + + /** + * The WebGL location for this program's 'scaleOverScaleDepth' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.scaleOverScaleDepthLocation = this.uniformLocation(gl, "scaleOverScaleDepth"); + + this.scratchArray9 = new Float32Array(9); +} + +/** + * A string that uniquely identifies this program. + * @type {string} + * @readonly + */ +AtmosphereProgram.key = "WorldWindGpuAtmosphereProgram"; + +// Inherit from GpuProgram. +AtmosphereProgram.prototype = Object.create(GpuProgram.prototype); + +/** + * Returns the atmosphere's altitude. + * @returns {Number} The atmosphere's altitude in meters. + */ +AtmosphereProgram.prototype.getAltitude = function () { + return this.altitude; +}; + +/** + * Loads the specified number as the value of this program's 'fragMode' uniform variable. + * + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Number} fragMode The frag mode value. + * @throws {ArgumentError} If the specified number is null or undefined. + */ +AtmosphereProgram.prototype.loadFragMode = function (gl, fragMode) { + if (!fragMode) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "AtmosphereProgram", "loadFragMode", "missingFragMode")); + } + + gl.uniform1i(this.fragModeLocation, fragMode); +}; + +/** + * Loads the specified matrix as the value of this program's 'mvpMatrix' uniform variable. + * + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Matrix} matrix The matrix to load. + * @throws {ArgumentError} If the specified matrix is null or undefined. + */ +AtmosphereProgram.prototype.loadModelviewProjection = function (gl, matrix) { + if (!matrix) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "AtmosphereProgram", "loadModelviewProjection", + "missingMatrix")); + } + + this.loadUniformMatrix(gl, matrix, this.mvpMatrixLocation); +}; + +/** + * Loads the specified vector as the value of this program's 'vertexOrigin' uniform variable. + * + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Vec3} vector The vector to load. + * @throws {ArgumentError} If the specified vector is null or undefined. + */ +AtmosphereProgram.prototype.loadVertexOrigin = function (gl, vector) { + if (!vector) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "AtmosphereProgram", "loadVertexOrigin", "missingVector")); + } + + gl.uniform3f(this.vertexOriginLocation, vector[0], vector[1], vector[2]); +}; + +/** + * Loads the specified vector as the value of this program's 'lightDirection' uniform variable. + * + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Vec3} vector The vector to load. + * @throws {ArgumentError} If the specified vector is null or undefined. + */ +AtmosphereProgram.prototype.loadLightDirection = function (gl, vector) { + if (!vector) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "AtmosphereProgram", "loadLightDirection", "missingVector")); + } + + gl.uniform3f(this.lightDirectionLocation, vector[0], vector[1], vector[2]); +}; + +/** + * Loads the specified vector as the value of this program's 'lightDirection' uniform variable, + * the magnitude's specified vector as the value of this program's 'eyeMagnitude' uniform variable and + * the squared magnitude's specified vector as the value of this program's 'eyeMagnitude2' uniform variable. + * + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Vec3} vector The vector to load. + * @throws {ArgumentError} If the specified vector is null or undefined. + */ +AtmosphereProgram.prototype.loadEyePoint = function (gl, vector) { + if (!vector) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "AtmosphereProgram", "loadEyePoint", "missingVector")); + } + + gl.uniform3f(this.eyePointLocation, vector[0], vector[1], vector[2]); + gl.uniform1f(this.eyeMagnitudeLocation, vector.magnitude()); + gl.uniform1f(this.eyeMagnitude2Location, vector.magnitudeSquared()); +}; + +/** + * Loads the specified number as the value of this program's 'globeRadius' uniform variable and the specified + * number which add the altitude value as the value of this program's 'atmosphereRadius' uniform variable. + * + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Number} globeRadius The globe radius value. + * @throws {ArgumentError} If the specified number is null or undefined. + */ +AtmosphereProgram.prototype.loadGlobeRadius = function (gl, globeRadius) { + if (!globeRadius) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "AtmosphereProgram", "loadGlobeRadius", + "missingGlobeRadius")); + } + + var gr = globeRadius; + var ar = gr + this.altitude; + + gl.uniform1f(this.globeRadiusLocation, gr); + gl.uniform1f(this.atmosphereRadiusLocation, ar); + gl.uniform1f(this.atmosphereRadius2Location, ar * ar); +}; + +/** + * Sets the program's 'scale', 'scaleDepth' and 'scaleOverScaleDepth' uniform variables. + * + * @param {WebGLRenderingContext} gl The current WebGL context. + */ +AtmosphereProgram.prototype.setScale = function (gl) { + gl.uniform1f(this.scaleLocation, 1 / this.getAltitude()); + gl.uniform1f(this.scaleDepthLocation, this.rayleighScaleDepth); + gl.uniform1f(this.scaleOverScaleDepthLocation, 1 / this.getAltitude() / this.rayleighScaleDepth); +}; + +/** + * Loads the specified matrix as the value of this program's 'texCoordMatrix' uniform variable. + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Matrix3} matrix The texture coordinate matrix. + */ +AtmosphereProgram.prototype.loadTexMatrix = function (gl, matrix) { + if (!matrix) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "AtmosphereProgram", "loadTexMatrix", + "missingMatrix")); + } + + matrix.columnMajorComponents(this.scratchArray9); + gl.uniformMatrix3fv(this.texCoordMatrixLocation, false, this.scratchArray9); +}; + +export default AtmosphereProgram; diff --git a/web/test/WebWorldWind/src/shaders/BasicProgram.js b/web/test/WebWorldWind/src/shaders/BasicProgram.js new file mode 100644 index 00000000..98c8a83e --- /dev/null +++ b/web/test/WebWorldWind/src/shaders/BasicProgram.js @@ -0,0 +1,127 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports BasicProgram + */ +import ArgumentError from '../error/ArgumentError'; +import Color from '../util/Color'; +import GpuProgram from '../shaders/GpuProgram'; +import Logger from '../util/Logger'; +import BasicVertex from './glsl/basic_vertex.glsl'; +import BasicFragment from './glsl/basic_fragment.glsl'; + +/** + * Constructs a new program. + * Initializes, compiles and links this GLSL program with the source code for its vertex and fragment shaders. + *
+ * This method creates WebGL shaders for the program's shader sources and attaches them to a new GLSL program. This + * method then compiles the shaders and then links the program if compilation is successful. Use the bind method to make the + * program current during rendering. + * + * @alias BasicProgram + * @constructor + * @augments GpuProgram + * @classdesc BasicProgram is a GLSL program that draws geometry in a solid color. + * @param {WebGLRenderingContext} gl The current WebGL context. + * @throws {ArgumentError} If the shaders cannot be compiled, or linking of + * the compiled shaders into a program fails. + */ +function BasicProgram(gl) { + var vertexShaderSource = BasicVertex, + fragmentShaderSource = BasicFragment; + + // Call to the superclass, which performs shader program compiling and linking. + GpuProgram.call(this, gl, vertexShaderSource, fragmentShaderSource); + + /** + * The WebGL location for this program's 'vertexPoint' attribute. + * @type {Number} + * @readonly + */ + this.vertexPointLocation = this.attributeLocation(gl, "vertexPoint"); + + /** + * The WebGL location for this program's 'mvpMatrix' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.mvpMatrixLocation = this.uniformLocation(gl, "mvpMatrix"); + + /** + * The WebGL location for this program's 'color' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.colorLocation = this.uniformLocation(gl, "color"); +} + +/** + * A string that uniquely identifies this program. + * @type {string} + * @readonly + */ +BasicProgram.key = "WorldWindGpuBasicProgram"; + +// Inherit from GpuProgram. +BasicProgram.prototype = Object.create(GpuProgram.prototype); + +/** + * Loads the specified matrix as the value of this program's 'mvpMatrix' uniform variable. + * + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Matrix} matrix The matrix to load. + * @throws {ArgumentError} If the specified matrix is null or undefined. + */ +BasicProgram.prototype.loadModelviewProjection = function (gl, matrix) { + if (!matrix) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "BasicProgram", "loadModelviewProjection", "missingMatrix")); + } + + this.loadUniformMatrix(gl, matrix, this.mvpMatrixLocation); +}; + +/** + * Loads the specified color as the value of this program's 'color' uniform variable. + * + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Color} color The color to load. + * @throws {ArgumentError} If the specified color is null or undefined. + */ +BasicProgram.prototype.loadColor = function (gl, color) { + if (!color) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "BasicProgram", "loadColor", "missingColor")); + } + + this.loadUniformColor(gl, color, this.colorLocation); +}; + +/** + * Loads the specified RGBA color components as the value of this program's 'color' uniform variable. + * + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Number} red The red component, a number between 0 and 1. + * @param {Number} green The green component, a number between 0 and 1. + * @param {Number} blue The blue component, a number between 0 and 1. + * @param {Number} alpha The alpha component, a number between 0 and 1. + */ +BasicProgram.prototype.loadColorComponents = function (gl, red, green, blue, alpha) { + this.loadUniformColorComponents(gl, red, green, blue, alpha, this.colorLocation); +}; + +export default BasicProgram; diff --git a/web/test/WebWorldWind/src/shaders/BasicTextureProgram.js b/web/test/WebWorldWind/src/shaders/BasicTextureProgram.js new file mode 100644 index 00000000..1bb6bb67 --- /dev/null +++ b/web/test/WebWorldWind/src/shaders/BasicTextureProgram.js @@ -0,0 +1,256 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports BasicTextureProgram + */ +import ArgumentError from '../error/ArgumentError'; +import Color from '../util/Color'; +import GpuProgram from '../shaders/GpuProgram'; +import Logger from '../util/Logger'; +import BasicTextureVertex from './glsl/basic_texture_vertex.glsl'; +import BasicTextureFragment from './glsl/basic_texture_fragment.glsl'; + +/** + * Constructs a new program. + * Initializes, compiles and links this GLSL program with the source code for its vertex and fragment shaders. + *
+ * This method creates WebGL shaders for the program's shader sources and attaches them to a new GLSL program. This + * method then compiles the shaders and then links the program if compilation is successful. Use the bind method to make the + * program current during rendering. + * + * @alias BasicTextureProgram + * @constructor + * @augments GpuProgram + * @classdesc BasicTextureProgram is a GLSL program that draws textured or untextured geometry. + * @param {WebGLRenderingContext} gl The current WebGL context. + * @throws {ArgumentError} If the shaders cannot be compiled, or if linking of + * the compiled shaders into a program fails. + */ +function BasicTextureProgram(gl) { + var vertexShaderSource = BasicTextureVertex, + fragmentShaderSource = BasicTextureFragment; + + // Specify bindings to avoid the WebGL performance warning that's generated when normalVector gets + // bound to location 0. + var bindings = ["vertexPoint", "normalVector", "vertexTexCoord"]; + + // Call to the superclass, which performs shader program compiling and linking. + GpuProgram.call(this, gl, vertexShaderSource, fragmentShaderSource, bindings); + + /** + * The WebGL location for this program's 'vertexPoint' attribute. + * @type {Number} + * @readonly + */ + this.vertexPointLocation = this.attributeLocation(gl, "vertexPoint"); + + /** + * The WebGL location for this program's 'normalVector' attribute. + * @type {Number} + * @readonly + */ + this.normalVectorLocation = this.attributeLocation(gl, "normalVector"); + + /** + * The WebGL location for this program's 'vertexTexCoord' attribute. + * @type {Number} + * @readonly + */ + this.vertexTexCoordLocation = this.attributeLocation(gl, "vertexTexCoord"); + + /** + * The WebGL location for this program's 'mvpMatrix' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.mvpMatrixLocation = this.uniformLocation(gl, "mvpMatrix"); + + /** + * The WebGL location for this program's 'mvInverseMatrix' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.mvInverseMatrixLocation = this.uniformLocation(gl, "mvInverseMatrix"); + + /** + * The WebGL location for this program's 'color' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.colorLocation = this.uniformLocation(gl, "color"); + + /** + * The WebGL location for this program's 'enableTexture' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.textureEnabledLocation = this.uniformLocation(gl, "enableTexture"); + + /** + * The WebGL location for this program's 'modulateColor' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.modulateColorLocation = this.uniformLocation(gl, "modulateColor"); + + /** + * The WebGL location for this program's 'textureSampler' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.textureUnitLocation = this.uniformLocation(gl, "textureSampler"); + + /** + * The WebGL location for this program's 'texCoordMatrix' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.textureMatrixLocation = this.uniformLocation(gl, "texCoordMatrix"); + + /** + * The WebGL location for this program's 'opacity' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.opacityLocation = this.uniformLocation(gl, "opacity"); + + /** + * The WegGL location for this program's 'enableLighting' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.applyLightingLocation = this.uniformLocation(gl, "applyLighting"); +} + +/** + * A string that uniquely identifies this program. + * @type {string} + * @readonly + */ +BasicTextureProgram.key = "WorldWindGpuBasicTextureProgram"; + +// Inherit from GpuProgram. +BasicTextureProgram.prototype = Object.create(GpuProgram.prototype); + +/** + * Loads the specified matrix as the value of this program's 'mvInverseMatrix' uniform variable. + * + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Matrix} matrix The matrix to load. + * @throws {ArgumentError} If the specified matrix is null or undefined. + */ +BasicTextureProgram.prototype.loadModelviewInverse = function (gl, matrix) { + if (!matrix) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "BasicTextureProgram", "loadModelviewInverse", "missingMatrix")); + } + + this.loadUniformMatrix(gl, matrix, this.mvInverseMatrixLocation); +}; + +/** + * Loads the specified matrix as the value of this program's 'mvpMatrix' uniform variable. + * + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Matrix} matrix The matrix to load. + * @throws {ArgumentError} If the specified matrix is null or undefined. + */ +BasicTextureProgram.prototype.loadModelviewProjection = function (gl, matrix) { + if (!matrix) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "BasicTextureProgram", "loadModelviewProjection", "missingMatrix")); + } + + this.loadUniformMatrix(gl, matrix, this.mvpMatrixLocation); +}; + +/** + * Loads the specified color as the value of this program's 'color' uniform variable. + * + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Color} color The color to load. + * @throws {ArgumentError} If the specified color is null or undefined. + */ +BasicTextureProgram.prototype.loadColor = function (gl, color) { + if (!color) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "BasicTextureProgram", "loadColor", "missingColor")); + } + + this.loadUniformColor(gl, color, this.colorLocation); +}; + +/** + * Loads the specified boolean as the value of this program's 'enableTexture' uniform variable. + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Boolean} enable true to enable texturing, false to disable texturing. + */ +BasicTextureProgram.prototype.loadTextureEnabled = function (gl, enable) { + gl.uniform1i(this.textureEnabledLocation, enable ? 1 : 0); +}; + +/** + * Loads the specified boolean as the value of this program's 'modulateColor' uniform variable. When this + * value is true and the value of the textureEnabled variable is true, the color uniform of this shader is + * multiplied by the rounded alpha component of the texture color at each fragment. This causes the color + * to be either fully opaque or fully transparent depending on the value of the texture color's alpha value. + * This is used during picking to replace opaque or mostly opaque texture colors with the pick color, and + * to make all other texture colors transparent. + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Boolean} enable true to enable modulation, false to disable modulation. + */ +BasicTextureProgram.prototype.loadModulateColor = function (gl, enable) { + gl.uniform1i(this.modulateColorLocation, enable ? 1 : 0); +}; + +/** + * Loads the specified number as the value of this program's 'textureSampler' uniform variable. + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Number} unit The texture unit. + */ +BasicTextureProgram.prototype.loadTextureUnit = function (gl, unit) { + gl.uniform1i(this.textureUnitLocation, unit - gl.TEXTURE0); +}; + +/** + * Loads the specified matrix as the value of this program's 'texCoordMatrix' uniform variable. + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Matrix} matrix The texture coordinate matrix. + */ +BasicTextureProgram.prototype.loadTextureMatrix = function (gl, matrix) { + this.loadUniformMatrix(gl, matrix, this.textureMatrixLocation); +}; + +/** + * Loads the specified number as the value of this program's 'opacity' uniform variable. + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Number} opacity The opacity in the range [0, 1]. + */ +BasicTextureProgram.prototype.loadOpacity = function (gl, opacity) { + gl.uniform1f(this.opacityLocation, opacity); +}; + +/** + * Loads the specified boolean as the value of this program's 'applyLighting' uniform variable. + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Number} applyLighting true to apply lighting, otherwise false. + */ +BasicTextureProgram.prototype.loadApplyLighting = function (gl, applyLighting) { + gl.uniform1i(this.applyLightingLocation, applyLighting); +}; + +export default BasicTextureProgram; diff --git a/web/test/WebWorldWind/src/shaders/GpuProgram.js b/web/test/WebWorldWind/src/shaders/GpuProgram.js new file mode 100644 index 00000000..a149779b --- /dev/null +++ b/web/test/WebWorldWind/src/shaders/GpuProgram.js @@ -0,0 +1,281 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports GpuProgram + */ +import ArgumentError from '../error/ArgumentError'; +import Color from '../util/Color'; +import GpuShader from '../shaders/GpuShader'; +import Logger from '../util/Logger'; + + +/** + * Constructs a GPU program with specified source code for vertex and fragment shaders. + * This constructor is intended to be called only by subclasses. + *
+ * This constructor creates WebGL shaders for the specified shader sources and attaches them to a new GLSL + * program. The method compiles the shaders and then links the program if compilation is successful. Use the + * [DrawContext.bindProgram]{@link DrawContext#bindProgram} function to make the program current during rendering. + * + * @alias GpuProgram + * @constructor + * @classdesc + * Represents an OpenGL shading language (GLSL) shader program and provides methods for identifying and + * accessing shader variables. Shader programs are created by instances of this class and made current when the + * DrawContext.bindProgram function is invoked. + *
+ * This is an abstract class and not intended to be created directly. + * + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {String} vertexShaderSource The source code for the vertex shader. + * @param {String} fragmentShaderSource The source code for the fragment shader. + * @param {String[]} attributeBindings An array of attribute variable names whose bindings are to be explicitly + * specified. Each name is bound to its corresponding index in the array. May be null, in which case the + * linker determines all the bindings. + * @throws {ArgumentError} If either source is null or undefined, the shaders cannot be compiled, or linking of + * the compiled shaders into a program fails. + */ +function GpuProgram(gl, vertexShaderSource, fragmentShaderSource, attributeBindings) { + if (!vertexShaderSource || !fragmentShaderSource) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "GpuProgram", "constructor", + "The specified shader source is null or undefined.")); + } + + var program, vShader, fShader; + + try { + vShader = new GpuShader(gl, gl.VERTEX_SHADER, vertexShaderSource); + fShader = new GpuShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); + } catch (e) { + if (vShader) + vShader.dispose(gl); + if (fShader) + fShader.dispose(gl); + + throw e; + } + + program = gl.createProgram(); + if (!program) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "GpuProgram", "constructor", + "Unable to create shader program.")); + } + + gl.attachShader(program, vShader.shaderId); + gl.attachShader(program, fShader.shaderId); + + if (attributeBindings) { + for (var i = 0, len = attributeBindings.length; i < len; i++) { + gl.bindAttribLocation(program, i, attributeBindings[i]); + } + } + + if (!this.link(gl, program)) { + // Get the info log before deleting the program. + var infoLog = gl.getProgramInfoLog(program); + + gl.detachShader(program, vShader.shaderId); + gl.detachShader(program, fShader.shaderId); + gl.deleteProgram(program); + vShader.dispose(gl); + fShader.dispose(gl); + + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "GpuProgram", "constructor", + "Unable to link shader program: " + infoLog)); + } + + /** + * Indicates the WebGL program object associated with this GPU program. + * @type {WebGLProgram} + * @readonly + */ + this.programId = program; + + // Internal. Intentionally not documented. These will be filled in as attribute locations are requested. + this.attributeLocations = {}; + this.uniformLocations = {}; + + // Internal. Intentionally not documented. + this.vertexShader = vShader; + + // Internal. Intentionally not documented. + this.fragmentShader = fShader; + + // Internal. Intentionally not documented. + this.size = vertexShaderSource.length + fragmentShaderSource.length; + + // Internal. Intentionally not documented. + this.scratchArray = new Float32Array(16); +} + +/** + * Releases this GPU program's WebGL program and associated shaders. Upon return this GPU program's WebGL + * program ID is 0 as is that of the associated shaders. + * + * @param {WebGLRenderingContext} gl The current WebGL context. + */ +GpuProgram.prototype.dispose = function (gl) { + if (this.programId) { + if (this.vertexShader) { + gl.detachShader(this.programId, this.vertexShader.shaderId); + } + if (this.fragmentShader) { + gl.detachShader(this.programId, this.fragmentShader.shaderId); + } + + gl.deleteProgram(this.programId); + delete this.programId; + } + + if (this.vertexShader) { + this.vertexShader.dispose(gl); + delete this.vertexShader; + } + + if (this.fragmentShader) { + this.fragmentShader.dispose(gl); + delete this.fragmentShader; + } + + this.attributeLocations = {}; + this.uniformLocations = {}; +}; + +/** + * Returns the GLSL attribute location of a specified attribute name. + * + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {String} attributeName The name of the attribute whose location is determined. + * @returns {Number} The WebGL attribute location of the specified attribute, or -1 if the attribute is not + * found. + * @throws {ArgumentError} If the specified attribute name is null, empty or undefined. + */ +GpuProgram.prototype.attributeLocation = function (gl, attributeName) { + if (!attributeName || attributeName.length == 0) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "GpuProgram", "attributeLocation", + "The specified attribute name is null, undefined or empty.")); + } + + var location = this.attributeLocations[attributeName]; + if (!location) { + location = gl.getAttribLocation(this.programId, attributeName); + this.attributeLocations[attributeName] = location; + } + + return location; +}; + +/** + * Returns the GLSL uniform location of a specified uniform name. + * + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {String} uniformName The name of the uniform variable whose location is determined. + * @returns {WebGLUniformLocation} The WebGL uniform location of the specified uniform variable, + * or -1 if the uniform is not found. + * @throws {ArgumentError} If the specified uniform name is null, empty or undefined. + */ +GpuProgram.prototype.uniformLocation = function (gl, uniformName) { + if (!uniformName || uniformName.length == 0) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "GpuProgram", "uniformLocation", + "The specified uniform name is null, undefined or empty.")); + } + + var location = this.uniformLocations[uniformName]; + if (!location) { + location = gl.getUniformLocation(this.programId, uniformName); + this.uniformLocations[uniformName] = location; + } + + return location; +}; + +/** + * Links a specified GLSL program. This method is not meant to be called by applications. It is called + * internally as needed. + * + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {WebGLProgram} program The WebGL program. + * @returns {Boolean} true if linking was successful, otherwise false. + * @protected + */ +GpuProgram.prototype.link = function (gl, program) { + gl.linkProgram(program); + + return gl.getProgramParameter(program, gl.LINK_STATUS); +}; + +/** + * Loads a specified matrix as the value of a GLSL 4x4 matrix uniform variable with the specified location. + *
+ * This functions converts the matrix into column-major order prior to loading its components into the GLSL + * uniform variable, but does not modify the specified matrix. + * + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Matrix} matrix The matrix to load. + * @param {WebGLUniformLocation} location The location of the uniform variable in the currently bound GLSL program. + * @throws {ArgumentError} If the specified matrix is null or undefined. + */ +GpuProgram.prototype.loadUniformMatrix = function (gl, matrix, location) { + if (!matrix) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "GpuProgram", "loadUniformMatrix", + "missingMatrix")); + } + + var columnMajorArray = matrix.columnMajorComponents(this.scratchArray); + gl.uniformMatrix4fv(location, false, columnMajorArray); +}; + +/** + * Loads a specified color as the value of a GLSL vec4 uniform variable with the specified location. + *
+ * This function multiplies the red, green and blue components by the alpha component prior to loading the color + * in the GLSL uniform variable, but does not modify the specified color. + * + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Color} color The color to load. + * @param {WebGLUniformLocation} location The location of the uniform variable in the currently bound GLSL program. + * @throws {ArgumentError} If the specified color is null or undefined. + */ +GpuProgram.prototype.loadUniformColor = function (gl, color, location) { + if (!color) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "GpuProgram", "loadUniformColor", + "missingColor")); + } + + var premul = color.premultipliedComponents(this.scratchArray); + gl.uniform4f(location, premul[0], premul[1], premul[2], premul[3]); +}; + +/** + * Loads the specified RGBA color components as the value of a GLSL vec4 uniform variable with the specified + * location. + *
+ * This function multiplies the red, green and blue components by the alpha component prior to loading the color
+ * in the GLSL uniform variable.
+ *
+ * @param {WebGLRenderingContext} gl The current WebGL context.
+ * @param {Number} red The red component, a number between 0 and 1.
+ * @param {Number} green The green component, a number between 0 and 1.
+ * @param {Number} blue The blue component, a number between 0 and 1.
+ * @param {Number} alpha The alpha component, a number between 0 and 1.
+ * @param {WebGLUniformLocation} location The location of the uniform variable in the currently bound GLSL program.
+ */
+GpuProgram.prototype.loadUniformColorComponents = function (gl, red, green, blue, alpha, location) {
+ gl.uniform4f(location, red * alpha, green * alpha, blue * alpha, alpha);
+};
+
+export default GpuProgram;
diff --git a/web/test/WebWorldWind/src/shaders/GpuShader.js b/web/test/WebWorldWind/src/shaders/GpuShader.js
new file mode 100644
index 00000000..85a4b1eb
--- /dev/null
+++ b/web/test/WebWorldWind/src/shaders/GpuShader.js
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports GpuShader
+ */
+import ArgumentError from '../error/ArgumentError';
+import Logger from '../util/Logger';
+
+
+/**
+ * Constructs a GPU shader of a specified type with specified GLSL source code.
+ *
+ * @alias GpuShader
+ * @constructor
+ * @classdesc
+ * Represents an OpenGL shading language (GLSL) shader and provides methods for compiling and disposing
+ * of them.
+ *
+ * @param {WebGLRenderingContext} gl The current WebGL context.
+ * @param {Number} shaderType The type of shader, either WebGLRenderingContext.VERTEX_SHADER
+ * or WebGLRenderingContext.FRAGMENT_SHADER.
+ * @param {String} shaderSource The shader's source code.
+ * @throws {ArgumentError} If the shader type is unrecognized, the shader source is null or undefined or shader
+ * compilation fails. If the compilation fails the error thrown contains any compilation messages.
+ */
+function GpuShader(gl, shaderType, shaderSource) {
+ if (!(shaderType === gl.VERTEX_SHADER
+ || shaderType === gl.FRAGMENT_SHADER)) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "GpuShader", "constructor",
+ "The specified shader type is unrecognized."));
+ }
+
+ if (!shaderSource) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "GpuShader", "constructor",
+ "The specified shader source is null or undefined."));
+ }
+
+ var shader = gl.createShader(shaderType);
+ if (!shader) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "GpuShader", "constructor",
+ "Unable to create shader of type " +
+ (shaderType == gl.VERTEX_SHADER ? "VERTEX_SHADER." : "FRAGMENT_SHADER.")));
+ }
+
+ if (!this.compile(gl, shader, shaderType, shaderSource)) {
+ var infoLog = gl.getShaderInfoLog(shader);
+
+ gl.deleteShader(shader);
+
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "GpuShader", "constructor",
+ "Unable to compile shader: " + infoLog));
+ }
+
+ this.shaderId = shader;
+}
+
+/**
+ * Compiles the source code for this shader. This method is not meant to be invoked by applications. It is
+ * invoked internally as needed.
+ * @param {WebGLRenderingContext} gl The current WebGL rendering context.
+ * @param {WebGLShader} shaderId The shader ID.
+ * @param {Number} shaderType The type of shader, either WebGLRenderingContext.VERTEX_SHADER
+ * or WebGLRenderingContext.FRAGMENT_SHADER.
+ * @param {String} shaderSource The shader's source code.
+ * @returns {boolean} true if the shader compiled successfully, otherwise false.
+ */
+GpuShader.prototype.compile = function (gl, shaderId, shaderType, shaderSource) {
+ gl.shaderSource(shaderId, shaderSource);
+ gl.compileShader(shaderId);
+
+ return gl.getShaderParameter(shaderId, gl.COMPILE_STATUS);
+};
+
+/**
+ * Releases this shader's WebGL shader.
+ * @param {WebGLRenderingContext} gl The current WebGL rendering context.
+ */
+GpuShader.prototype.dispose = function (gl) {
+ if (this.shaderId) {
+ gl.deleteShader(this.shaderId);
+ delete this.shaderId;
+ }
+};
+
+export default GpuShader;
diff --git a/web/test/WebWorldWind/src/shaders/GroundProgram.js b/web/test/WebWorldWind/src/shaders/GroundProgram.js
new file mode 100644
index 00000000..dd765763
--- /dev/null
+++ b/web/test/WebWorldWind/src/shaders/GroundProgram.js
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports GroundProgram
+ */
+import AtmosphereProgram from '../shaders/AtmosphereProgram';
+import GroundVertex from './glsl/ground_vertex.glsl';
+import GroundFragment from './glsl/ground_fragment.glsl';
+
+/**
+ * Constructs a new program.
+ * Initializes, compiles and links this GLSL program with the source code for its vertex and fragment shaders.
+ *
+ * This method creates WebGL shaders for the program's shader sources and attaches them to a new GLSL program. + * This method then compiles the shaders and then links the program if compilation is successful. Use the bind + * method to make the program current during rendering. + * + * @alias GroundProgram + * @constructor + * @augments AtmosphereProgram + * @classdesc GroundProgram is a GLSL program that draws the ground component of the atmosphere. + * @param {WebGLRenderingContext} gl The current WebGL context. + * @throws {ArgumentError} If the shaders cannot be compiled, or linking of + * the compiled shaders into a program fails. + */ +function GroundProgram(gl) { + var vertexShaderSource = GroundVertex, + fragmentShaderSource = GroundFragment; + + // Call to the superclass, which performs shader program compiling and linking. + AtmosphereProgram.call(this, gl, vertexShaderSource, fragmentShaderSource, ["vertexPoint", "vertexTexCoord"]); +} + +/** + * A string that uniquely identifies this program. + * @type {string} + * @readonly + */ +GroundProgram.key = "WorldWindGroundProgram"; + +// Inherit from AtmosphereProgram. +GroundProgram.prototype = Object.create(AtmosphereProgram.prototype); + +export default GroundProgram; + + + diff --git a/web/test/WebWorldWind/src/shaders/SkyProgram.js b/web/test/WebWorldWind/src/shaders/SkyProgram.js new file mode 100644 index 00000000..2be00430 --- /dev/null +++ b/web/test/WebWorldWind/src/shaders/SkyProgram.js @@ -0,0 +1,62 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports SkyProgram + */ +import AtmosphereProgram from '../shaders/AtmosphereProgram'; +import SkyVertex from './glsl/sky_vertex.glsl'; +import SkyFragment from './glsl/sky_fragment.glsl'; + +/** + * Constructs a new program. + * Initializes, compiles and links this GLSL program with the source code for its vertex and fragment shaders. + *
+ * This method creates WebGL shaders for the program's shader sources and attaches them to a new GLSL program. + * This method then compiles the shaders and then links the program if compilation is successful. Use the bind + * method to make the program current during rendering. + * + * @alias SkyProgram + * @constructor + * @augments AtmosphereProgram + * @classdesc SkyProgram is a GLSL program that draws the sky component of the atmosphere. + * @param {WebGLRenderingContext} gl The current WebGL context. + * @throws {ArgumentError} If the shaders cannot be compiled, or linking of + * the compiled shaders into a program fails. + */ +function SkyProgram(gl) { + var vertexShaderSource = SkyVertex, + fragmentShaderSource = SkyFragment; + + // Call to the superclass, which performs shader program compiling and linking. + AtmosphereProgram.call(this, gl, vertexShaderSource, fragmentShaderSource, ["vertexPoint"]); +} + +/** + * A string that uniquely identifies this program. + * @type {string} + * @readonly + */ +SkyProgram.key = "WorldWindSkyProgram"; + +// Inherit from AtmosphereProgram. +SkyProgram.prototype = Object.create(AtmosphereProgram.prototype); + +export default SkyProgram; + + + + diff --git a/web/test/WebWorldWind/src/shaders/StarFieldProgram.js b/web/test/WebWorldWind/src/shaders/StarFieldProgram.js new file mode 100644 index 00000000..cf95a6d9 --- /dev/null +++ b/web/test/WebWorldWind/src/shaders/StarFieldProgram.js @@ -0,0 +1,172 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports StarFieldProgram + */ +import ArgumentError from '../error/ArgumentError'; +import GpuProgram from '../shaders/GpuProgram'; +import Logger from '../util/Logger'; +import StarFieldVertex from './glsl/star_field_vertex.glsl'; +import StarFieldFragment from './glsl/star_field_fragment.glsl'; + +/** + * Constructs a new program. + * Initializes, compiles and links this GLSL program with the source code for its vertex and fragment shaders. + *
+ * This method creates WebGL shaders for the program's shader sources and attaches them to a new GLSL program. + * This method then compiles the shaders and then links the program if compilation is successful. + * Use the bind method to make the program current during rendering. + * + * @alias StarFieldProgram + * @constructor + * @augments GpuProgram + * @classdesc StarFieldProgram is a GLSL program that draws points representing stars. + * @param {WebGLRenderingContext} gl The current WebGL context. + * @throws {ArgumentError} If the shaders cannot be compiled, or linking of the compiled shaders into a program + * fails. + */ +function StarFieldProgram(gl) { + var vertexShaderSource = StarFieldVertex, + fragmentShaderSource = StarFieldFragment; + + // Call to the superclass, which performs shader program compiling and linking. + GpuProgram.call(this, gl, vertexShaderSource, fragmentShaderSource, ["vertexPoint"]); + + /** + * The WebGL location for this program's 'vertexPoint' attribute. + * @type {Number} + * @readonly + */ + this.vertexPointLocation = this.attributeLocation(gl, "vertexPoint"); + + /** + * The WebGL location for this program's 'mvpMatrix' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.mvpMatrixLocation = this.uniformLocation(gl, "mvpMatrix"); + + /** + * The WebGL location for this program's 'numDays' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.numDaysLocation = this.uniformLocation(gl, "numDays"); + + /** + * The WebGL location for this program's 'magnitudeRangeLocation' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.magnitudeRangeLocation = this.uniformLocation(gl, "magnitudeRange"); + + /** + * The WebGL location for this program's 'textureSampler' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.textureUnitLocation = this.uniformLocation(gl, "textureSampler"); + + /** + * The WebGL location for this program's 'textureEnabled' uniform. + * @type {WebGLUniformLocation} + * @readonly + */ + this.textureEnabledLocation = this.uniformLocation(gl, "textureEnabled"); +} + +/** + * A string that uniquely identifies this program. + * @type {string} + * @readonly + */ +StarFieldProgram.key = "WorldWindGpuStarFieldProgram"; + +// Inherit from GpuProgram. +StarFieldProgram.prototype = Object.create(GpuProgram.prototype); + +/** + * Loads the specified matrix as the value of this program's 'mvpMatrix' uniform variable. + * + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Matrix} matrix The matrix to load. + * @throws {ArgumentError} If the specified matrix is null or undefined. + */ +StarFieldProgram.prototype.loadModelviewProjection = function (gl, matrix) { + if (!matrix) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "StarFieldProgram", "loadModelviewProjection", "missingMatrix")); + } + + this.loadUniformMatrix(gl, matrix, this.mvpMatrixLocation); +}; + +/** + * Loads the specified number as the value of this program's 'numDays' uniform variable. + * + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Number} numDays The number of days (positive or negative) since Greenwich noon, Terrestrial Time, + * on 1 January 2000 (J2000.0) + * @throws {ArgumentError} If the specified number is null or undefined. + */ +StarFieldProgram.prototype.loadNumDays = function (gl, numDays) { + if (numDays == null) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "StarFieldProgram", "loadNumDays", "missingNumDays")); + } + gl.uniform1f(this.numDaysLocation, numDays); +}; + +/** + * Loads the specified numbers as the value of this program's 'magnitudeRange' uniform variable. + * + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Number} minMag + * @param {Number} maxMag + * @throws {ArgumentError} If the specified numbers are null or undefined. + */ +StarFieldProgram.prototype.loadMagnitudeRange = function (gl, minMag, maxMag) { + if (minMag == null) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "StarFieldProgram", "loadMagRange", "missingMinMag")); + } + if (maxMag == null) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "StarFieldProgram", "loadMagRange", "missingMaxMag")); + } + gl.uniform2f(this.magnitudeRangeLocation, minMag, maxMag); +}; + +/** + * Loads the specified number as the value of this program's 'textureSampler' uniform variable. + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Number} unit The texture unit. + */ +StarFieldProgram.prototype.loadTextureUnit = function (gl, unit) { + gl.uniform1i(this.textureUnitLocation, unit - gl.TEXTURE0); +}; + +/** + * Loads the specified boolean as the value of this program's 'textureEnabledLocation' uniform variable. + * @param {WebGLRenderingContext} gl The current WebGL context. + * @param {Boolean} value + */ +StarFieldProgram.prototype.loadTextureEnabled = function (gl, value) { + gl.uniform1i(this.textureEnabledLocation, value ? 1 : 0); +}; + +export default StarFieldProgram; diff --git a/web/test/WebWorldWind/src/shaders/SurfaceTileRendererProgram.js b/web/test/WebWorldWind/src/shaders/SurfaceTileRendererProgram.js new file mode 100644 index 00000000..b1ffd315 --- /dev/null +++ b/web/test/WebWorldWind/src/shaders/SurfaceTileRendererProgram.js @@ -0,0 +1,212 @@ +/* + * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the + * National Aeronautics and Space Administration. All rights reserved. + * + * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @exports SurfaceTileRendererProgram + */ +import ArgumentError from '../error/ArgumentError'; +import Color from '../util/Color'; +import GpuProgram from '../shaders/GpuProgram'; +import Logger from '../util/Logger'; +import SurfaceTileVertex from './glsl/surface_tile_vertex.glsl'; +import SurfaceTileFragment from './glsl/surface_tile_fragment.glsl'; + +/** + * Constructs a new surface-tile-renderer program. + * Initializes, compiles and links this GLSL program with the source code for its vertex and fragment shaders. + *
+ * This method creates WebGL shaders for the program's shader sources and attaches them to a new GLSL program. This
+ * method then compiles the shaders and links the program if compilation is successful. Use the bind method to make the
+ * program current during rendering.
+ *
+ * @alias SurfaceTileRendererProgram
+ * @constructor
+ * @augments GpuProgram
+ * @classdesc A GLSL program that draws textured geometry on the globe's terrain.
+ * Application's typically do not interact with this class.
+ * @param {WebGLRenderingContext} gl The current WebGL context.
+ */
+function SurfaceTileRendererProgram(gl) {
+ var vertexShaderSource = SurfaceTileVertex,
+ fragmentShaderSource = SurfaceTileFragment;
+
+ // Call to the superclass, which performs shader program compiling and linking.
+ GpuProgram.call(this, gl, vertexShaderSource, fragmentShaderSource);
+
+ // Capture the attribute and uniform locations.
+
+ /**
+ * This program's vertex point location.
+ * @type {Number}
+ * @readonly
+ */
+ this.vertexPointLocation = this.attributeLocation(gl, "vertexPoint");
+
+ /**
+ * This program's texture coordinate location.
+ * @type {Number}
+ * @readonly
+ */
+ this.vertexTexCoordLocation = this.attributeLocation(gl, "vertexTexCoord");
+
+ /**
+ * This program's modelview-projection matrix location.
+ * @type {WebGLUniformLocation}
+ * @readonly
+ */
+ this.mvpMatrixLocation = this.uniformLocation(gl, "mvpMatrix");
+
+ /**
+ * The WebGL location for this program's 'color' uniform.
+ * @type {WebGLUniformLocation}
+ * @readonly
+ */
+ this.colorLocation = this.uniformLocation(gl, "color");
+
+ /**
+ * The WebGL location for this program's 'modulateColor' uniform.
+ * @type {WebGLUniformLocation}
+ * @readonly
+ */
+ this.modulateColorLocation = this.uniformLocation(gl, "modulateColor");
+
+ // The rest of these are strictly internal and intentionally not documented.
+ this.texSamplerMatrixLocation = this.uniformLocation(gl, "texSamplerMatrix");
+ this.texMaskMatrixLocation = this.uniformLocation(gl, "texMaskMatrix");
+ this.texSamplerLocation = this.uniformLocation(gl, "texSampler");
+ this.opacityLocation = this.uniformLocation(gl, "opacity");
+
+ /**
+ * The WebGL location for this program's 'vertexTexCoord' attribute.
+ * @type {Number}
+ * @readonly
+ */
+ this.vertexPointLocation = -1;
+}
+
+/**
+ * A string that uniquely identifies this program.
+ * @type {string}
+ * @readonly
+ */
+SurfaceTileRendererProgram.key = "WorldWindGpuSurfaceTileRenderingProgram";
+
+SurfaceTileRendererProgram.prototype = Object.create(GpuProgram.prototype);
+
+/**
+ * Loads the specified matrix as the value of this program's 'mvpMatrix' uniform variable.
+ *
+ * @param {WebGLRenderingContext} gl The current WebGL context.
+ * @param {Matrix} matrix The matrix to load.
+ * @throws {ArgumentError} If the specified matrix is null or undefined.
+ */
+SurfaceTileRendererProgram.prototype.loadModelviewProjection = function (gl, matrix) {
+ if (!matrix) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceTileRendererProgram", "loadModelviewProjection",
+ "missingMatrix"));
+ }
+
+ this.loadUniformMatrix(gl, matrix, this.mvpMatrixLocation);
+};
+
+/**
+ * Loads the specified matrix as the value of this program's 'texSamplerMatrix' uniform variable.
+ *
+ * @param {WebGLRenderingContext} gl The current WebGL context.
+ * @param {Matrix} matrix The matrix to load.
+ * @throws {ArgumentError} If the specified matrix is null or undefined.
+ */
+SurfaceTileRendererProgram.prototype.loadTexSamplerMatrix = function (gl, matrix) {
+ if (!matrix) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceTileRendererProgram", "loadTexSamplerMatrix",
+ "missingMatrix"));
+ }
+
+ this.loadUniformMatrix(gl, matrix, this.texSamplerMatrixLocation);
+};
+
+/**
+ * Loads the specified matrix as the value of this program's 'texMaskMatrix' uniform variable.
+ *
+ * @param {WebGLRenderingContext} gl The current WebGL context.
+ * @param {Matrix} matrix The matrix to load.
+ * @throws {ArgumentError} If the specified matrix is null or undefined.
+ */
+SurfaceTileRendererProgram.prototype.loadTexMaskMatrix = function (gl, matrix) {
+ if (!matrix) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceTileRendererProgram", "loadTexMaskMatrix",
+ "missingMatrix"));
+ }
+
+ this.loadUniformMatrix(gl, matrix, this.texMaskMatrixLocation);
+};
+
+/**
+ * Loads the specified texture unit ID as the value of this program's 'texSampler' uniform variable.
+ * The specified unit ID must be one of the GL_TEXTUREi WebGL enumerations, where i ranges from 0 to
+ * GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS - 1.
+ *
+ * @param {WebGLRenderingContext} gl The current WebGL context.
+ * @param {Number} unit The unit ID to load.
+ */
+SurfaceTileRendererProgram.prototype.loadTexSampler = function (gl, unit) {
+ gl.uniform1i(this.texSamplerLocation, unit - WebGLRenderingContext.TEXTURE0);
+};
+
+/**
+ * Loads the specified value as the value of this program's 'opacity' uniform variable.
+ *
+ * @param {WebGLRenderingContext} gl The current WebGL context.
+ * @param {Number} opacity The opacity to load.
+ */
+SurfaceTileRendererProgram.prototype.loadOpacity = function (gl, opacity) {
+ gl.uniform1f(this.opacityLocation, opacity);
+};
+
+/**
+ * Loads the specified color as the value of this program's 'color' uniform variable.
+ *
+ * @param {WebGLRenderingContext} gl The current WebGL context.
+ * @param {Color} color The color to load.
+ * @throws {ArgumentError} If the specified color is null or undefined.
+ */
+SurfaceTileRendererProgram.prototype.loadColor = function (gl, color) {
+ if (!color) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceTileRendererProgram", "loadColor", "missingColor"));
+ }
+
+ this.loadUniformColor(gl, color, this.colorLocation);
+};
+
+/**
+ * Loads the specified boolean as the value of this program's 'modulateColor' uniform variable. When this
+ * value is true the color uniform of this shader is
+ * multiplied by the rounded alpha component of the texture color at each fragment. This causes the color
+ * to be either fully opaque or fully transparent depending on the value of the texture color's alpha value.
+ * This is used during picking to replace opaque or mostly opaque texture colors with the pick color, and
+ * to make all other texture colors transparent.
+ * @param {WebGLRenderingContext} gl The current WebGL context.
+ * @param {Boolean} enable
+ * In order to support simultaneous use of this shape by multiple windows and 2D globes, this shape
+ * maintains a cache of data computed relative to the globe displayed in each window. 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 data cache entry.
+ *
+ * @param {ShapeAttributes} attributes The attributes to associate with this shape. May be null, in which case
+ * default attributes are associated.
+ */
+function AbstractShape(attributes) {
+
+ Renderable.call(this);
+
+ // Documented with its property accessor below.
+ this._attributes = attributes ? attributes : new ShapeAttributes(null);
+
+ // Documented with its property accessor below.
+ this._highlightAttributes = null;
+
+ /**
+ * Indicates whether this shape uses its normal attributes or its highlight attributes when displayed.
+ * If true, the highlight attributes are used, otherwise the normal attributes are used. The normal
+ * attributes are also used if no highlight attributes have been specified.
+ * @type {Boolean}
+ * @default false
+ */
+ this.highlighted = false;
+
+ // Private. See defined property below for documentation.
+ this._altitudeMode = WorldWind.ABSOLUTE;
+
+ // Internal use only. Intentionally not documented.
+ // A position used to compute relative coordinates for the shape.
+ this.referencePosition = null;
+
+ // Internal use only. Intentionally not documented.
+ // Holds the per-globe data generated during makeOrderedRenderable.
+ this.shapeDataCache = new MemoryCache(3, 2);
+
+ // Internal use only. Intentionally not documented.
+ // The shape-data-cache data that is for the currently active globe. This field is made current prior to
+ // calls to makeOrderedRenderable and doRenderOrdered.
+ this.currentData = null;
+
+ // Internal use only. Intentionally not documented.
+ this.activeAttributes = null;
+
+ /**
+ * Indicates how long to use terrain-specific shape data before regenerating it, in milliseconds. A value
+ * of zero specifies that shape data should be regenerated every frame. While this causes the shape to
+ * adapt more frequently to the terrain, it decreases performance.
+ * @type {Number}
+ * @default 2000 (milliseconds)
+ */
+ this.expirationInterval = 2000;
+
+ /**
+ * Indicates whether to use a surface shape to represent this shape when drawn on a 2D globe.
+ * @type {Boolean}
+ * @default false
+ */
+ this.useSurfaceShapeFor2D = false;
+
+ this.scratchMatrix = Matrix.fromIdentity(); // scratch variable
+}
+
+AbstractShape.prototype = Object.create(Renderable.prototype);
+
+Object.defineProperties(AbstractShape.prototype, {
+ /**
+ * This shape's normal (non-highlight) attributes.
+ * @type {ShapeAttributes}
+ * @memberof AbstractShape.prototype
+ */
+ attributes: {
+ get: function () {
+ return this._attributes;
+ },
+ set: function (value) {
+ this._attributes = value;
+
+ if (this.surfaceShape) {
+ this.surfaceShape.attributes = this._attributes;
+ }
+ }
+ },
+
+ /**
+ * This shape's highlight attributes. If null or undefined and this shape's highlight flag is true, this
+ * shape's normal attributes are used. If they in turn are null or undefined, this shape is not drawn.
+ * @type {ShapeAttributes}
+ * @default null
+ * @memberof AbstractShape.prototype
+ */
+ highlightAttributes: {
+ get: function () {
+ return this._highlightAttributes;
+ },
+ set: function (value) {
+ this._highlightAttributes = value;
+
+ if (this.surfaceShape) {
+ this.surfaceShape.highlightAttributes = this._highlightAttributes;
+ }
+ }
+ },
+
+ /**
+ * The altitude mode to use when drawing this shape. Recognized values are:
+ *
+ * Altitudes within the mesh's positions are interpreted according to the mesh's altitude mode, which
+ * can be one of the following:
+ *
+ * Meshes have separate attributes for normal display and highlighted display. They use the interior and
+ * outline attributes of {@link ShapeAttributes}. If those attributes identify an image, that image is
+ * applied to the mesh. Texture coordinates for the image may be specified, but if not specified the full
+ * image is stretched over the full mesh. If texture coordinates are specified, there must be one texture
+ * coordinate for each vertex in the mesh.
+ *
+ * @param {Position[][]} positions A two-dimensional array containing the mesh vertices.
+ * Each entry of the array specifies the vertices of one row of the mesh. The arrays for all rows must
+ * have the same length. There must be at least two rows, and each row must have at least two vertices.
+ * There must be no more than 65536 positions.
+ * @param {ShapeAttributes} attributes The attributes to associate with this mesh. May be null, in which case
+ * default attributes are associated.
+ *
+ * @throws {ArgumentError} If the specified positions array is null or undefined, the number of rows or the
+ * number of vertices per row is less than 2, the array lengths are inconsistent, or too many positions are
+ * specified (limit is 65536).
+ */
+function GeographicMesh(positions, attributes) {
+ if (!positions) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "GeographicMesh", "constructor", "missingPositions"));
+ }
+
+ if (positions.length < 2 || positions[0].length < 2) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "GeographicMesh", "constructor",
+ "Number of positions is insufficient."));
+ }
+
+ // Check for size limit, which is the max number of available indices for a 16-bit unsigned int.
+ if (positions.length * positions[0].length > 65536) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "GeographicMesh", "constructor",
+ "Too many positions. Must be fewer than 65536. Try using multiple meshes."));
+ }
+
+ var length = positions[0].length;
+ for (var i = 1; i < positions.length; i++) {
+ if (positions[i].length !== length) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "GeographicMesh", "constructor",
+ "Array lengths are inconsistent."));
+ }
+ }
+
+ var numRows = positions.length,
+ numCols = positions[0].length;
+
+ AbstractMesh.call(this, attributes);
+
+ /**
+ * Indicates whether this mesh is pickable when the pick point intersects transparent pixels of the
+ * image applied to this mesh. If no image is applied to this mesh, this property is ignored. If this
+ * property is true and an image with fully transparent pixels is applied to the mesh, the mesh is
+ * pickable at those transparent pixels, otherwise this mesh is not pickable at those transparent pixels.
+ * @type {Boolean}
+ * @default true
+ */
+ this.pickTransparentImagePixels = true;
+
+ // Private. Documentation is with the defined property below and the constructor description above.
+ this._positions = positions;
+
+ // Private. Documentation is with the defined property below.
+ this._altitudeScale = 1;
+
+ // Internal. Intentionally not documented.
+ this.numRows = numRows;
+ this.numColumns = numCols;
+
+ // Internal. Intentionally not documented.
+ this._textureCoordinates = null;
+
+ // Internal. Intentionally not documented.
+ this.referencePosition = this.determineReferencePosition(this._positions);
+}
+
+GeographicMesh.prototype = Object.create(AbstractMesh.prototype);
+
+Object.defineProperties(GeographicMesh.prototype, {
+ /**
+ * This mesh's positions. Each array in the positions array specifies the geographic positions of one
+ * row of the mesh.
+ *
+ * @type {Position[][]}
+ * @memberof GeographicMesh.prototype
+ */
+ positions: {
+ get: function () {
+ return this._positions;
+ },
+ set: function (positions) {
+ if (!positions) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "GeographicMesh", "positions", "missingPositions"));
+ }
+
+ if (positions.length < 2 || positions[0].length < 2) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "GeographicMesh", "positions",
+ "Number of positions is insufficient."));
+ }
+
+ var length = positions[0].length;
+ for (var i = 1; i < positions.length; i++) {
+ if (positions[i].length !== length) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "GeographicMesh", "positions",
+ "Array lengths are inconsistent."));
+ }
+ }
+
+ this.numRows = positions.length;
+ this.numColumns = positions[0].length;
+
+ this._positions = positions;
+ this.referencePosition = this.determineReferencePosition(this._positions);
+ this.reset();
+
+ this.meshIndices = null;
+ this.outlineIndices = null;
+ }
+ },
+
+ /**
+ * This mesh's texture coordinates if this mesh is textured. A texture coordinate must be
+ * provided for each mesh position. The texture coordinates are specified as a two-dimensional array,
+ * each entry of which specifies the texture coordinates for one row of the mesh. Each texture coordinate
+ * is a {@link Vec2} containing the s and t coordinates. If no texture coordinates are specified and
+ * the attributes associated with this mesh indicate an image source, then texture coordinates are
+ * automatically generated for the mesh.
+ * @type {Vec2[][]}
+ * @default null
+ * @memberof GeographicMesh.prototype
+ */
+ textureCoordinates: {
+ get: function () {
+ return this._textureCoordinates;
+ },
+ set: function (coords) {
+
+ if (coords && coords.length != this.numRows) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "GeographicMesh", "textureCoordinates",
+ "Number of texture coordinate rows is inconsistent with the currently specified positions."));
+ }
+
+ for (var i = 0; i < this.numRows; i++) {
+ if (coords[i].length !== this.numColumns) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "GeographicMesh", "textureCoordinates",
+ "Texture coordinate row lengths are inconsistent with the currently specified positions."));
+ }
+ }
+
+ this._textureCoordinates = coords;
+ this.reset();
+ this.texCoords = null;
+ }
+ }
+});
+
+GeographicMesh.makeGridIndices = function (nRows, nCols) {
+ // Compute indices for individual triangles.
+
+ var gridIndices = [],
+ i = 0;
+
+ for (var r = 0; r < nRows - 1; r++) {
+ for (var c = 0; c < nCols - 1; c++) {
+ var k = r * nCols + c;
+
+ gridIndices[i++] = k;
+ gridIndices[i++] = k + 1;
+ gridIndices[i++] = k + nCols;
+ gridIndices[i++] = k + 1;
+ gridIndices[i++] = k + 1 + nCols;
+ gridIndices[i++] = k + nCols;
+ }
+ }
+
+ return gridIndices;
+};
+
+// Intentionally not documented.
+GeographicMesh.prototype.determineReferencePosition = function (positions) {
+ // Assign the first position as the reference position.
+ return positions[0][0];
+};
+
+// Overridden from AbstractShape base class.
+GeographicMesh.prototype.createSurfaceShape = function () {
+ var boundaries = [];
+
+ for (var c = 0; c < this.numColumns; c++) {
+ boundaries.push(this._positions[0][c]);
+ }
+
+ for (var r = 1; r < this.numRows; r++) {
+ boundaries.push(this._positions[r][this.numColumns - 1]);
+ }
+
+ for (c = this.numColumns - 2; c >= 0; c--) {
+ boundaries.push(this._positions[this.numRows - 1][c]);
+ }
+
+ for (r = this.numRows - 2; r > 0; r--) {
+ boundaries.push(this._positions[r][0]);
+ }
+
+ return new SurfacePolygon(boundaries, null);
+};
+
+GeographicMesh.prototype.computeMeshPoints = function (dc, currentData) {
+ // Unwrap the mesh row arrays into one long array.
+
+ var eyeDistSquared = Number.MAX_VALUE,
+ eyePoint = dc.eyePoint,
+ meshPoints = new Float32Array(this.numRows * this.numColumns * 3),
+ pt = new Vec3(0, 0, 0),
+ k = 0,
+ pos, dSquared;
+
+ for (var r = 0; r < this._positions.length; r++) {
+ for (var c = 0, len = this._positions[r].length; c < len; c++) {
+ pos = this._positions[r][c];
+
+ dc.surfacePointForMode(pos.latitude, pos.longitude, pos.altitude * this._altitudeScale,
+ this.altitudeMode, pt);
+
+ dSquared = pt.distanceToSquared(eyePoint);
+ if (dSquared < eyeDistSquared) {
+ eyeDistSquared = dSquared;
+ }
+
+ pt.subtract(this.currentData.referencePoint);
+
+ meshPoints[k++] = pt[0];
+ meshPoints[k++] = pt[1];
+ meshPoints[k++] = pt[2];
+ }
+ }
+
+ currentData.eyeDistance = Math.sqrt(eyeDistSquared);
+
+ return meshPoints;
+};
+
+GeographicMesh.prototype.computeTexCoords = function () {
+ if (this._textureCoordinates) {
+ return this.computeExplicitTexCoords();
+ } else {
+ return this.computeImplicitTexCoords();
+ }
+};
+
+// Intentionally not documented.
+GeographicMesh.prototype.computeExplicitTexCoords = function () {
+ // Capture the texture coordinates to a single array parallel to the mesh points array.
+
+ var texCoords = new Float32Array(2 * this.numRows * this.numColumns),
+ k = 0;
+
+ for (var r = 0; r < this._textureCoordinates.length; r++) {
+ for (var c = 0, len = this._textureCoordinates[r].length; c < len; c++) {
+ var texCoord = this._textureCoordinates[r][c];
+
+ texCoords[k++] = texCoord[0];
+ texCoords[k++] = texCoord[1];
+ }
+ }
+
+ return texCoords;
+};
+
+// Intentionally not documented.
+GeographicMesh.prototype.computeImplicitTexCoords = function () {
+ // Create texture coordinates that map the full image source into the full mesh.
+
+ var texCoords = new Float32Array(2 * this.numRows * this.numColumns),
+ rowDelta = 1.0 / (this.numRows - 1),
+ columnDelta = 1.0 / (this.numColumns - 1),
+ k = 0;
+
+ for (var r = 0; r < this._positions.length; r++) {
+ var t = r === this.numRows - 1 ? 1.0 : r * rowDelta;
+
+ for (var c = 0, len = this._positions[r].length; c < len; c++) {
+ texCoords[k++] = c === this.numColumns - 1 ? 1.0 : c * columnDelta;
+ texCoords[k++] = t;
+ }
+ }
+
+ return texCoords;
+};
+
+GeographicMesh.prototype.computeMeshIndices = function () {
+ // Compute indices for individual triangles.
+
+ var meshIndices = new Uint16Array((this.numRows - 1) * (this.numColumns - 1) * 6),
+ i = 0;
+
+ for (var r = 0; r < this.numRows - 1; r++) {
+ for (var c = 0; c < this.numColumns - 1; c++) {
+ var k = r * this.numColumns + c;
+
+ meshIndices[i++] = k;
+ meshIndices[i++] = k + 1;
+ meshIndices[i++] = k + this.numColumns;
+ meshIndices[i++] = k + 1;
+ meshIndices[i++] = k + 1 + this.numColumns;
+ meshIndices[i++] = k + this.numColumns;
+ }
+ }
+
+ return meshIndices;
+};
+
+GeographicMesh.prototype.computeOutlineIndices = function () {
+ // Walk the mesh boundary and capture those positions for the outline.
+
+ var outlineIndices = new Uint16Array(2 * this.numRows + 2 * this.numColumns),
+ k = 0;
+
+ for (var c = 0; c < this.numColumns; c++) {
+ outlineIndices[k++] = c;
+ }
+
+ for (var r = 1; r < this.numRows; r++) {
+ outlineIndices[k++] = (r + 1) * this.numColumns - 1;
+ }
+
+ for (c = this.numRows * this.numColumns - 2; c >= (this.numRows - 1) * this.numColumns; c--) {
+ outlineIndices[k++] = c;
+ }
+
+ for (r = this.numRows - 2; r >= 0; r--) {
+ outlineIndices[k++] = r * this.numColumns;
+ }
+
+ return outlineIndices;
+};
+
+export default GeographicMesh;
diff --git a/web/test/WebWorldWind/src/shapes/GeographicText.js b/web/test/WebWorldWind/src/shapes/GeographicText.js
new file mode 100644
index 00000000..5f485b2e
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/GeographicText.js
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports GeographicText
+ */
+import ArgumentError from '../error/ArgumentError';
+import Logger from '../util/Logger';
+import Text from '../shapes/Text';
+import Vec3 from '../geom/Vec3';
+
+
+/**
+ * Constructs a geographic text shape at a specified position.
+ * @alias GeographicText
+ * @constructor
+ * @augments Text
+ * @classdesc Represents a string of text displayed at a geographic position.
+ *
+ * See also {@link ScreenText}.
+ *
+ * @param {Position} position The text's geographic position.
+ * @param {String} text The text to display.
+ * @throws {ArgumentError} If either the specified position or text is null or undefined.
+ */
+function GeographicText(position, text) {
+ if (!position) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Text", "constructor", "missingPosition"));
+ }
+
+ Text.call(this, text);
+
+ /**
+ * This text's geographic position.
+ * The [TextAttributes.offset]{@link TextAttributes#offset} property indicates the relationship of the
+ * text string to this position.
+ * @type {Position}
+ */
+ this.position = position;
+
+ /**
+ * Indicates the group ID of the declutter group to include this Text shape. This shape
+ * is decluttered relative to all other shapes within its group by the default
+ * [declutter filter]{@link WorldWindow#declutter}. To prevent decluttering of this shape, set its
+ * declutter group to 0.
+ * @type {Number}
+ * @default 1
+ */
+ this.declutterGroup = 1;
+}
+
+// Internal use only. Intentionally not documented.
+GeographicText.placePoint = new Vec3(0, 0, 0); // Cartesian point corresponding to this placemark's geographic position
+
+GeographicText.prototype = Object.create(Text.prototype);
+
+/**
+ * Creates a new geographic text object that is a copy of this one.
+ * @returns {GeographicText} The new geographic text object.
+ */
+GeographicText.prototype.clone = function () {
+ var clone = new GeographicText(this.position, this.text);
+
+ clone.copy(this);
+ clone.pickDelegate = this.pickDelegate ? this.pickDelegate : this;
+
+ return clone;
+};
+
+// Documented in superclass.
+GeographicText.prototype.render = function (dc) {
+ // Filter out instances outside any projection limits.
+ if (dc.globe.projectionLimits
+ && !dc.globe.projectionLimits.containsLocation(this.position.latitude, this.position.longitude)) {
+ return;
+ }
+
+ Text.prototype.render.call(this, dc);
+};
+
+// Documented in superclass.
+GeographicText.prototype.computeScreenPointAndEyeDistance = function (dc) {
+ // Compute the text's model point and corresponding distance to the eye point.
+ dc.surfacePointForMode(this.position.latitude, this.position.longitude, this.position.altitude,
+ this.altitudeMode, GeographicText.placePoint);
+
+ if (!dc.frustumInModelCoordinates.containsPoint(GeographicText.placePoint)) {
+ return false;
+ }
+
+ this.eyeDistance = this.alwaysOnTop ? 0 : dc.eyePoint.distanceTo(GeographicText.placePoint);
+
+ // Compute the text's screen point in the OpenGL coordinate system of the WorldWindow by projecting its model
+ // coordinate point onto the viewport. Apply a depth offset in order to cause the text to appear above nearby
+ // terrain. When text is displayed near the terrain portions of its geometry are often behind the terrain,
+ // yet as a screen element the text is expected to be visible. We adjust its depth values rather than moving
+ // the text itself to avoid obscuring its actual position.
+ if (!dc.projectWithDepth(GeographicText.placePoint, this.depthOffset, this.screenPoint)) {
+ return false;
+ }
+
+ return true;
+};
+
+export default GeographicText;
diff --git a/web/test/WebWorldWind/src/shapes/Path.js b/web/test/WebWorldWind/src/shapes/Path.js
new file mode 100644
index 00000000..da48c04a
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/Path.js
@@ -0,0 +1,624 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports Path
+ */
+import AbstractShape from '../shapes/AbstractShape';
+import ArgumentError from '../error/ArgumentError';
+import BasicTextureProgram from '../shaders/BasicTextureProgram';
+import BoundingBox from '../geom/BoundingBox';
+import Color from '../util/Color';
+import Location from '../geom/Location';
+import Logger from '../util/Logger';
+import Matrix from '../geom/Matrix';
+import PickedObject from '../pick/PickedObject';
+import Position from '../geom/Position';
+import ShapeAttributes from '../shapes/ShapeAttributes';
+import SurfacePolyline from '../shapes/SurfacePolyline';
+import Vec2 from '../geom/Vec2';
+import Vec3 from '../geom/Vec3';
+
+
+/**
+ * Constructs a path.
+ * @alias Path
+ * @constructor
+ * @augments AbstractShape
+ * @classdesc Represents a line, curve or curtain between specified positions. The path is drawn between input
+ * positions to achieve a specified path type, which can be one of the following:
+ *
+ * Paths conform to the terrain if the path's [followTerrain]{@link Path#followTerrain} property is true.
+ *
+ * Altitudes within the path's positions are interpreted according to the path's altitude mode, which
+ * can be one of the following:
+ *
+ * Paths have separate attributes for normal display and highlighted display. They use the interior and
+ * outline attributes of {@link ShapeAttributes} but do not use the image attributes.
+ *
+ * A path displays as a curtain if its [extrude]{@link Path#extrude} property is true. A curtain extends
+ * from the line formed by the path positions to the ground.
+ *
+ * This shape uses a {@link SurfacePolyline} when drawing on 2D globes and this shape's
+ * [useSurfaceShapeFor2D]{@link AbstractShape#useSurfaceShapeFor2D} is true.
+ *
+ * @param {Position[]} positions An array containing the path positions.
+ * @param {ShapeAttributes} attributes The attributes to associate with this path. May be null, in which case
+ * default attributes are associated.
+ * @throws {ArgumentError} If the specified positions array is null or undefined.
+ */
+function Path(positions, attributes) {
+ if (!positions) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Path", "constructor", "missingPositions"));
+ }
+
+ AbstractShape.call(this, attributes);
+
+ // Private. Documentation is with the defined property below.
+ this._positions = positions;
+
+ // Private. Documentation is with the defined property below.
+ this._pathType = WorldWind.GREAT_CIRCLE;
+
+ // Private. Documentation is with the defined property below.
+ this._terrainConformance = 10;
+
+ // Private. Documentation is with the defined property below.
+ this._numSubSegments = 10;
+
+ this.referencePosition = this.determineReferencePosition(this._positions);
+
+ this.scratchPoint = new Vec3(0, 0, 0); // scratch variable
+}
+
+Path.prototype = Object.create(AbstractShape.prototype);
+
+Object.defineProperties(Path.prototype, {
+ /**
+ * This path's positions.
+ * @type {Position[]}
+ * @memberof Path.prototype
+ */
+ positions: {
+ get: function () {
+ return this._positions;
+ },
+ set: function (positions) {
+ if (!positions) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Path", "constructor", "missingPositions"));
+ }
+
+ this._positions = positions;
+ this.referencePosition = this.determineReferencePosition(this._positions);
+ this.reset();
+ }
+ },
+
+ /**
+ * Indicates whether this path should conform to the terrain.
+ * @type {Boolean}
+ * @default false
+ * @memberof Path.prototype
+ */
+ followTerrain: {
+ get: function () {
+ return this._followTerrain;
+ },
+ set: function (followTerrain) {
+ this._followTerrain = followTerrain;
+ this.reset();
+ }
+ },
+
+ /**
+ * 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.
+ * @type {Number}
+ * @default 10
+ * @memberof Path.prototype
+ */
+ terrainConformance: {
+ get: function () {
+ return this._terrainConformance;
+ },
+ set: function (terrainConformance) {
+ this._terrainConformance = terrainConformance;
+ this.reset();
+ }
+ },
+
+ /**
+ * Specifies the number of segments used between specified positions to achieve this path's path type. Higher values
+ * cause the path to conform more closely to the path type but decrease performance.
+ *
+ * Placemarks may be drawn with either an image or as single-color square with a specified size. When the
+ * placemark attributes indicate a valid image, the placemark's image is drawn as a rectangle in the
+ * image's original dimensions, scaled by the image scale attribute. Otherwise, the placemark is drawn as a
+ * square with width and height equal to the value of the image scale attribute, in pixels, and color equal
+ * to the image color attribute.
+ *
+ * By default, placemarks participate in decluttering with a [declutterGroupID]{@link Placemark#declutterGroup}
+ * of 2. Only placemark labels are decluttered relative to other placemark labels. The placemarks themselves
+ * are optionally scaled with eye distance to achieve decluttering of the placemark as a whole.
+ * See [eyeDistanceScaling]{@link Placemark#eyeDistanceScaling}.
+ * @param {Position} position The placemark's geographic position.
+ * @param {Boolean} eyeDistanceScaling Indicates whether the size of this placemark scales with eye distance.
+ * See [eyeDistanceScalingThreshold]{@link Placemark#eyeDistanceScalingThreshold} and
+ * [eyeDistanceScalingLabelThreshold]{@link Placemark#eyeDistanceScalingLabelThreshold}.
+ * @param {PlacemarkAttributes} attributes The attributes to associate with this placemark. May be null,
+ * in which case default attributes are associated.
+ * @throws {ArgumentError} If the specified position is null or undefined.
+ */
+function Placemark(position, eyeDistanceScaling, attributes) {
+ if (!position) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Placemark", "constructor", "missingPosition"));
+ }
+
+ Renderable.call(this);
+
+ /**
+ * The placemark's attributes. If null and this placemark is not highlighted, this placemark is not
+ * drawn.
+ * @type {PlacemarkAttributes}
+ * @default see [PlacemarkAttributes]{@link PlacemarkAttributes}
+ */
+ this.attributes = attributes ? attributes : new PlacemarkAttributes(null);
+
+ /**
+ * The attributes used when this placemark's highlighted flag is true. If null and the
+ * highlighted flag is true, this placemark's normal attributes are used. If they, too, are null, this
+ * placemark is not drawn.
+ * @type {PlacemarkAttributes}
+ * @default null
+ */
+ this.highlightAttributes = null;
+
+ /**
+ * Indicates whether this placemark uses its highlight attributes rather than its normal attributes.
+ * @type {Boolean}
+ * @default false
+ */
+ this.highlighted = false;
+
+ /**
+ * This placemark's geographic position.
+ * @type {Position}
+ */
+ this.position = position;
+
+ /**
+ * Indicates whether this placemark's size is reduced at higher eye distances. If true, this placemark's
+ * size is scaled inversely proportional to the eye distance if the eye distance is greater than the
+ * value of the [eyeDistanceScalingThreshold]{@link Placemark#eyeDistanceScalingThreshold} property.
+ * When the eye distance is below the threshold, this placemark is scaled only according to the
+ * [imageScale]{@link PlacemarkAttributes#imageScale}.
+ * @type {Boolean}
+ */
+ this.eyeDistanceScaling = eyeDistanceScaling;
+
+ /**
+ * The eye distance above which to reduce the size of this placemark, in meters. If
+ * [eyeDistanceScaling]{@link Placemark#eyeDistanceScaling} is true, this placemark's image, label and leader
+ * line sizes are reduced as the eye distance increases beyond this threshold.
+ * @type {Number}
+ * @default 1e6 (meters)
+ */
+ this.eyeDistanceScalingThreshold = 1e6;
+
+ /**
+ * The eye altitude above which this placemark's label is not displayed.
+ * @type {number}
+ */
+ this.eyeDistanceScalingLabelThreshold = 1.5 * this.eyeDistanceScalingThreshold;
+
+ /**
+ * This placemark's textual label. If null, no label is drawn.
+ * @type {String}
+ * @default null
+ */
+ this.label = null;
+
+ /**
+ * This placemark's altitude mode. May be one of
+ *
+ * Altitudes within the polygon's positions are interpreted according to the polygon's altitude mode, which
+ * can be one of the following:
+ *
+ * Polygons have separate attributes for normal display and highlighted display. They use the interior and
+ * outline attributes of {@link ShapeAttributes}. If those attributes identify an image, that image is
+ * applied to the polygon.
+ *
+ * A polygon displays as a vertical prism if its [extrude]{@link Polygon#extrude} property is true. A
+ * curtain is formed around its boundaries and extends from the polygon's edges to the ground.
+ *
+ * A polygon can be textured, including its extruded boundaries. The textures are specified via the
+ * [imageSource]{@link ShapeAttributes#imageSource} property of the polygon's attributes. If that
+ * property is a single string or {@link ImageSource}, then it identifies the image source for the
+ * polygon's texture. If that property is an array of strings, {@link ImageSource}s or a combination of
+ * those, then the first entry in the array specifies the polygon's image source and subsequent entries
+ * specify the image sources of the polygon's extruded boundaries. If the array contains two entries, the
+ * first is the polygon's image source and the second is the common image source for all extruded
+ * boundaries. If the array contains more than two entries, then the first entry is the polygon's image
+ * source and each subsequent entry is the image source for consecutive extruded boundary segments. A null
+ * value for any entry indicates that no texture is applied for the corresponding polygon or extruded edge
+ * segment. If fewer image sources are specified then there are boundary segments, the last image source
+ * specified is applied to the remaining segments. Texture coordinates for the polygon's texture are
+ * specified via this polygon's [textureCoordinates]{@link Polygon#textureCoordinates} property. Texture
+ * coordinates for extruded boundary segments are implicitly defined to fit the full texture to each
+ * boundary segment.
+ *
+ * When displayed on a 2D globe, this polygon displays as a {@link SurfacePolygon} if its
+ * [useSurfaceShapeFor2D]{@link AbstractShape#useSurfaceShapeFor2D} property is true.
+ *
+ * @param {Position[][] | Position[]} boundaries A two-dimensional array containing the polygon boundaries.
+ * Each entry of the array specifies the vertices of one boundary.
+ * This argument may also be a simple array of positions,
+ * in which case the polygon is assumed to have only one boundary.
+ * Each boundary is considered implicitly closed, so the last position of the boundary need not and should not
+ * duplicate the first position of the boundary.
+ * @param {ShapeAttributes} attributes The attributes to associate with this polygon. May be null, in which case
+ * default attributes are associated.
+ *
+ * @throws {ArgumentError} If the specified boundaries array is null or undefined.
+ */
+function Polygon(boundaries, attributes) {
+ if (!boundaries) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Polygon", "constructor", "missingBoundaries"));
+ }
+
+ AbstractShape.call(this, attributes);
+
+ if (boundaries.length > 0 && boundaries[0].latitude) {
+ boundaries = [boundaries];
+ this._boundariesSpecifiedSimply = true;
+ }
+
+ // Private. Documentation is with the defined property below and the constructor description above.
+ this._boundaries = boundaries;
+
+ this._textureCoordinates = null;
+
+ this.referencePosition = this.determineReferencePosition(this._boundaries);
+
+ this._extrude = false;
+
+ this.scratchPoint = new Vec3(0, 0, 0); // scratch variable
+}
+
+Polygon.prototype = Object.create(AbstractShape.prototype);
+
+Object.defineProperties(Polygon.prototype, {
+ /**
+ * This polygon's boundaries. A two-dimensional array containing the polygon boundaries. Each entry of the
+ * array specifies the vertices of one boundary. This property may also be a simple
+ * array of positions, in which case the polygon is assumed to have only one boundary.
+ * @type {Position[][] | Position[]}
+ * @memberof Polygon.prototype
+ */
+ boundaries: {
+ get: function () {
+ return this._boundariesSpecifiedSimply ? this._boundaries[0] : this._boundaries;
+ },
+ set: function (boundaries) {
+ if (!boundaries) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Polygon", "boundaries", "missingBoundaries"));
+ }
+
+ if (boundaries.length > 0 && boundaries[0].latitude) {
+ boundaries = [boundaries];
+ this._boundariesSpecifiedSimply = true;
+ }
+
+ this._boundaries = boundaries;
+ this.referencePosition = this.determineReferencePosition(this._boundaries);
+ this.reset();
+ }
+ },
+
+ /**
+ * This polygon's texture coordinates if this polygon is to be textured. A texture coordinate must be
+ * provided for each boundary position. The texture coordinates are specified as a two-dimensional array,
+ * each entry of which specifies the texture coordinates for one boundary. Each texture coordinate is a
+ * {@link Vec2} containing the s and t coordinates.
+ * @type {Vec2[][]}
+ * @default null
+ * @memberof Polygon.prototype
+ */
+ textureCoordinates: {
+ get: function () {
+ return this._textureCoordinates;
+ },
+ set: function (value) {
+ this._textureCoordinates = value;
+ this.reset();
+ }
+ },
+
+ /**
+ * Specifies whether to extrude this polygon to the ground by drawing a filled interior from the polygon
+ * to the terrain. The filled interior uses this polygon's interior attributes.
+ * @type {Boolean}
+ * @default false
+ * @memberof Polygon.prototype
+ */
+ extrude: {
+ get: function () {
+ return this._extrude;
+ },
+ set: function (extrude) {
+ this._extrude = extrude;
+ this.reset();
+ }
+ }
+});
+
+// Intentionally not documented.
+Polygon.prototype.determineReferencePosition = function (boundaries) {
+ // Assign the first position as the reference position.
+ return boundaries.length > 0 && boundaries[0].length > 2 ? boundaries[0][0] : null;
+};
+
+// Internal. Determines whether this shape's geometry must be re-computed.
+Polygon.prototype.mustGenerateGeometry = function (dc) {
+ if (!this.currentData.boundaryPoints) {
+ return true;
+ }
+
+ if (this.currentData.drawInterior !== this.activeAttributes.drawInterior) {
+ return true;
+ }
+
+ if (this.altitudeMode === WorldWind.ABSOLUTE) {
+ return false;
+ }
+
+ return this.currentData.isExpired;
+};
+
+// Internal. Indicates whether this polygon should be textured.
+Polygon.prototype.hasCapTexture = function () {
+ return this.textureCoordinates && this.capImageSource();
+};
+
+// Internal. Determines source of this polygon's cap texture. See the class description above for the policy.
+Polygon.prototype.capImageSource = function () {
+ if (!this.activeAttributes.imageSource) {
+ return null;
+ }
+
+ if (typeof this.activeAttributes.imageSource === "string"
+ || this.activeAttributes.imageSource instanceof ImageSource) {
+ return this.activeAttributes.imageSource;
+ }
+
+ if (Array.isArray(this.activeAttributes.imageSource)
+ && this.activeAttributes.imageSource[0]
+ && (typeof this.activeAttributes.imageSource[0] === "string"
+ || this.activeAttributes.imageSource instanceof ImageSource)) {
+ return this.activeAttributes.imageSource[0];
+ }
+
+ return null;
+};
+
+// Internal. Indicates whether this polygon has side textures defined.
+Polygon.prototype.hasSideTextures = function () {
+ return this.activeAttributes.imageSource &&
+ Array.isArray(this.activeAttributes.imageSource) &&
+ this.activeAttributes.imageSource.length > 1;
+};
+
+// Internal. Determines the side texture for a specified side. See the class description above for the policy.
+Polygon.prototype.sideImageSource = function (side) {
+ if (side === 0 || this.activeAttributes.imageSource.length === 2) {
+ return this.activeAttributes.imageSource[1];
+ }
+
+ var numSideTextures = this.activeAttributes.imageSource.length - 1;
+ side = Math.min(side + 1, numSideTextures);
+ return this.activeAttributes.imageSource[side];
+};
+
+Polygon.prototype.createSurfaceShape = function () {
+ return new SurfacePolygon(this.boundaries, null);
+};
+
+// Overridden from AbstractShape base class.
+Polygon.prototype.doMakeOrderedRenderable = function (dc) {
+ // A null reference position is a signal that there are no boundaries to render.
+ if (!this.referencePosition) {
+ return null;
+ }
+
+ if (!this.activeAttributes.drawInterior && !this.activeAttributes.drawOutline) {
+ return null;
+ }
+
+ // See if the current shape data can be re-used.
+ if (!this.mustGenerateGeometry(dc)) {
+ return this;
+ }
+
+ var currentData = this.currentData;
+
+ // Set the transformation matrix to correspond to the reference position.
+ var refPt = currentData.referencePoint;
+ dc.surfacePointForMode(this.referencePosition.latitude, this.referencePosition.longitude,
+ this.referencePosition.altitude, this._altitudeMode, refPt);
+ currentData.transformationMatrix.setToTranslation(refPt[0], refPt[1], refPt[2]);
+
+ // Close the boundaries.
+ var fullBoundaries = [];
+ for (var b = 0; b < this._boundaries.length; b++) {
+ fullBoundaries[b] = this._boundaries[b].slice(0); // clones the array
+ fullBoundaries[b].push(this._boundaries[b][0]); // appends the first position to the boundary
+ }
+
+ // Convert the geographic coordinates to the Cartesian coordinates that will be rendered.
+ var boundaryPoints = this.computeBoundaryPoints(dc, fullBoundaries);
+
+ // Tessellate the polygon if its interior is to be drawn.
+ if (this.activeAttributes.drawInterior) {
+ var capVertices = this.tessellatePolygon(dc, boundaryPoints);
+ if (capVertices) {
+ // Must copy the vertices to a typed array. (Can't use typed array to begin with because its size
+ // is unknown prior to tessellation.)
+ currentData.capTriangles = new Float32Array(capVertices.length);
+ for (var i = 0, len = capVertices.length; i < len; i++) {
+ currentData.capTriangles[i] = capVertices[i];
+ }
+ }
+ }
+
+ currentData.boundaryPoints = boundaryPoints;
+ currentData.drawInterior = this.activeAttributes.drawInterior; // remember for validation
+ this.resetExpiration(currentData);
+ currentData.refreshBuffers = true; // causes VBOs to be reloaded
+
+ // Create the extent from the Cartesian points. Those points are relative to this path's reference point,
+ // so translate the computed extent to the reference point.
+ if (!currentData.extent) {
+ currentData.extent = new BoundingBox();
+ }
+ if (boundaryPoints.length === 1) {
+ currentData.extent.setToPoints(boundaryPoints[0]);
+ } else {
+ var allPoints = [];
+ for (b = 0; b < boundaryPoints.length; b++) {
+ for (var p = 0; p < boundaryPoints[b].length; p++) {
+ allPoints.push(boundaryPoints[b][p]);
+ }
+ }
+ currentData.extent.setToPoints(allPoints);
+ }
+ currentData.extent.translate(currentData.referencePoint);
+
+ return this;
+};
+
+// Private. Intentionally not documented.
+Polygon.prototype.computeBoundaryPoints = function (dc, boundaries) {
+ var eyeDistSquared = Number.MAX_VALUE,
+ eyePoint = dc.eyePoint,
+ boundaryPoints = [],
+ stride = this._extrude ? 6 : 3,
+ pt = new Vec3(0, 0, 0),
+ numBoundaryPoints, pos, k, dSquared;
+
+ for (var b = 0; b < boundaries.length; b++) {
+ numBoundaryPoints = (this._extrude ? 2 : 1) * boundaries[b].length;
+ boundaryPoints[b] = new Float32Array(numBoundaryPoints * 3);
+
+ for (var i = 0, len = boundaries[b].length; i < len; i++) {
+ pos = boundaries[b][i];
+
+ dc.surfacePointForMode(pos.latitude, pos.longitude, pos.altitude, this.altitudeMode, pt);
+
+ dSquared = pt.distanceToSquared(eyePoint);
+ if (dSquared < eyeDistSquared) {
+ eyeDistSquared = dSquared;
+ }
+
+ pt.subtract(this.currentData.referencePoint);
+
+ k = stride * i;
+ boundaryPoints[b][k] = pt[0];
+ boundaryPoints[b][k + 1] = pt[1];
+ boundaryPoints[b][k + 2] = pt[2];
+
+ if (this._extrude) {
+ dc.surfacePointForMode(pos.latitude, pos.longitude, 0, WorldWind.CLAMP_TO_GROUND, pt);
+
+ dSquared = pt.distanceToSquared(eyePoint);
+ if (dSquared < eyeDistSquared) {
+ eyeDistSquared = dSquared;
+ }
+
+ pt.subtract(this.currentData.referencePoint);
+
+ boundaryPoints[b][k + 3] = pt[0];
+ boundaryPoints[b][k + 4] = pt[1];
+ boundaryPoints[b][k + 5] = pt[2];
+ }
+ }
+ }
+
+ this.currentData.eyeDistance = 0;
+ /*DO NOT COMMITMath.sqrt(eyeDistSquared);*/
+
+ return boundaryPoints;
+};
+
+Polygon.prototype.tessellatePolygon = function (dc, boundaryPoints) {
+ var triangles = [], // the output list of triangles
+ error = 0,
+ stride = this._extrude ? 6 : 3,
+ includeTextureCoordinates = this.hasCapTexture(),
+ coords, normal;
+
+ if (!this.polygonTessellator) {
+ this.polygonTessellator = new libtess.GluTesselator();
+
+ this.polygonTessellator.gluTessCallback(libtess.gluEnum.GLU_TESS_VERTEX_DATA,
+ function (data, tris) {
+ tris[tris.length] = data[0];
+ tris[tris.length] = data[1];
+ tris[tris.length] = data[2];
+
+ if (includeTextureCoordinates) {
+ tris[tris.length] = data[3];
+ tris[tris.length] = data[4];
+ }
+ });
+
+ this.polygonTessellator.gluTessCallback(libtess.gluEnum.GLU_TESS_COMBINE,
+ function (coords, data, weight) {
+ var newCoords = [coords[0], coords[1], coords[2]];
+
+ if (includeTextureCoordinates) {
+ for (var i = 3; i <= 4; i++) {
+ var value = 0;
+ for (var w = 0; w < 4; w++) {
+ if (weight[w] > 0) {
+ value += weight[w] * data[w][i];
+ }
+ }
+
+ newCoords[i] = value;
+ }
+ }
+
+ return newCoords;
+ });
+
+ this.polygonTessellator.gluTessCallback(libtess.gluEnum.GLU_TESS_ERROR,
+ function (errno) {
+ error = errno;
+ Logger.logMessage(Logger.LEVEL_WARNING, "Polygon", "tessellatePolygon",
+ "Tessellation error " + errno + ".");
+ });
+ }
+
+ // Compute a normal vector for the polygon.
+ normal = Vec3.computeBufferNormal(boundaryPoints[0], stride);
+ if (!normal) {
+ normal = new Vec3(0, 0, 0);
+ // The first boundary is colinear. Fall back to the surface normal.
+ dc.globe.surfaceNormalAtLocation(this.referencePosition.latitude, this.referencePosition.longitude,
+ normal);
+ }
+ this.polygonTessellator.gluTessNormal(normal[0], normal[1], normal[2]);
+ this.currentData.capNormal = normal;
+
+ // Tessellate the polygon.
+ this.polygonTessellator.gluTessBeginPolygon(triangles);
+ for (var b = 0; b < boundaryPoints.length; b++) {
+ var t = 0;
+ this.polygonTessellator.gluTessBeginContour();
+ var contour = boundaryPoints[b];
+ for (var c = 0; c < contour.length; c += stride) {
+ coords = [contour[c], contour[c + 1], contour[c + 2]];
+ if (includeTextureCoordinates) {
+ if (t < this.textureCoordinates[b].length) {
+ coords[3] = this.textureCoordinates[b][t][0];
+ coords[4] = this.textureCoordinates[b][t][1];
+ } else {
+ coords[3] = this.textureCoordinates[b][0][0];
+ coords[4] = this.textureCoordinates[b][1][1];
+ }
+ ++t;
+ }
+ this.polygonTessellator.gluTessVertex(coords, coords);
+ }
+ this.polygonTessellator.gluTessEndContour();
+ }
+ this.polygonTessellator.gluTessEndPolygon();
+
+ return error === 0 ? triangles : null;
+};
+
+// Private. Intentionally not documented.
+Polygon.prototype.mustDrawVerticals = function (dc) {
+ return this._extrude
+ && this.activeAttributes.drawOutline
+ && this.activeAttributes.drawVerticals
+ && this.altitudeMode !== WorldWind.CLAMP_TO_GROUND;
+};
+
+// Overridden from AbstractShape base class.
+Polygon.prototype.doRenderOrdered = function (dc) {
+ var currentData = this.currentData,
+ pickColor;
+
+ if (dc.pickingMode) {
+ pickColor = dc.uniquePickColor();
+ }
+
+ // Draw the cap if the interior requested and we were able to tessellate the polygon.
+ if (this.activeAttributes.drawInterior && currentData.capTriangles && currentData.capTriangles.length > 0) {
+ this.drawCap(dc, pickColor);
+ }
+
+ if (this._extrude && this.activeAttributes.drawInterior) {
+ this.drawSides(dc, pickColor);
+ }
+
+ if (this.activeAttributes.drawOutline) {
+ this.drawOutline(dc, pickColor);
+ }
+
+ currentData.refreshBuffers = false;
+
+ if (dc.pickingMode) {
+ var po = new PickedObject(pickColor, this.pickDelegate ? this.pickDelegate : this, null,
+ this.layer, false);
+ dc.resolvePick(po);
+ }
+};
+
+Polygon.prototype.drawCap = function (dc, pickColor) {
+ var gl = dc.currentGlContext,
+ program = dc.currentProgram,
+ currentData = this.currentData,
+ refreshBuffers = currentData.refreshBuffers,
+ hasCapTexture = !!this.hasCapTexture(),
+ applyLighting = this.activeAttributes.applyLighting,
+ numCapVertices = currentData.capTriangles.length / (hasCapTexture ? 5 : 3),
+ vboId, color, stride, textureBound, capBuffer;
+
+ // Assume no cap texture.
+ program.loadTextureEnabled(gl, false);
+
+ this.applyMvpMatrix(dc);
+
+ if (!currentData.capVboCacheKey) {
+ currentData.capVboCacheKey = dc.gpuResourceCache.generateCacheKey();
+ }
+
+ vboId = dc.gpuResourceCache.resourceForKey(currentData.capVboCacheKey);
+ if (!vboId) {
+ vboId = gl.createBuffer();
+ dc.gpuResourceCache.putResource(currentData.capVboCacheKey, vboId, currentData.capTriangles.length * 4);
+ refreshBuffers = true;
+ }
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, vboId);
+ if (refreshBuffers) {
+ capBuffer = applyLighting ? this.makeCapBufferWithNormals() : currentData.capTriangles;
+ gl.bufferData(gl.ARRAY_BUFFER, capBuffer, gl.STATIC_DRAW);
+ dc.frameStatistics.incrementVboLoadCount(1);
+ }
+
+ color = this.activeAttributes.interiorColor;
+ // Disable writing the shape's fragments to the depth buffer when the interior is semi-transparent.
+ gl.depthMask(color.alpha * this.layer.opacity >= 1 || dc.pickingMode);
+ program.loadColor(gl, dc.pickingMode ? pickColor : color);
+ program.loadOpacity(gl, dc.pickingMode ? 1 : this.layer.opacity);
+
+ stride = 12 + (hasCapTexture ? 8 : 0) + (applyLighting ? 12 : 0);
+
+ if (hasCapTexture && !dc.pickingMode) {
+ this.activeTexture = dc.gpuResourceCache.resourceForKey(this.capImageSource());
+ if (!this.activeTexture) {
+ this.activeTexture =
+ dc.gpuResourceCache.retrieveTexture(dc.currentGlContext, this.capImageSource());
+ }
+
+ textureBound = this.activeTexture && this.activeTexture.bind(dc);
+ if (textureBound) {
+ gl.enableVertexAttribArray(program.vertexTexCoordLocation);
+ gl.vertexAttribPointer(program.vertexTexCoordLocation, 2, gl.FLOAT, false, stride, 12);
+
+ this.scratchMatrix.setToIdentity();
+ this.scratchMatrix.multiplyByTextureTransform(this.activeTexture);
+
+ program.loadTextureEnabled(gl, true);
+ program.loadTextureUnit(gl, gl.TEXTURE0);
+ program.loadTextureMatrix(gl, this.scratchMatrix);
+ program.loadModulateColor(gl, dc.pickingMode);
+ }
+ }
+
+ if (applyLighting && !dc.pickingMode) {
+ program.loadApplyLighting(gl, true);
+ gl.enableVertexAttribArray(program.normalVectorLocation);
+ gl.vertexAttribPointer(program.normalVectorLocation, 3, gl.FLOAT, false, stride, stride - 12);
+ }
+
+ gl.vertexAttribPointer(program.vertexPointLocation, 3, gl.FLOAT, false, stride, 0);
+ gl.drawArrays(gl.TRIANGLES, 0, numCapVertices);
+};
+
+Polygon.prototype.makeCapBufferWithNormals = function () {
+ var currentData = this.currentData,
+ normal = currentData.capNormal,
+ numFloatsIn = this.hasCapTexture() ? 5 : 3,
+ numFloatsOut = numFloatsIn + 3,
+ numVertices = currentData.capTriangles.length / numFloatsIn,
+ bufferIn = currentData.capTriangles,
+ bufferOut = new Float32Array(numVertices * numFloatsOut),
+ k = 0;
+
+ for (var i = 0; i < numVertices; i++) {
+ for (var j = 0; j < numFloatsIn; j++) {
+ bufferOut[k++] = bufferIn[i * numFloatsIn + j];
+ }
+
+ bufferOut[k++] = normal[0];
+ bufferOut[k++] = normal[1];
+ bufferOut[k++] = normal[2];
+ }
+
+ return bufferOut;
+};
+
+Polygon.prototype.drawSides = function (dc, pickColor) {
+ var gl = dc.currentGlContext,
+ program = dc.currentProgram,
+ currentData = this.currentData,
+ refreshBuffers = currentData.refreshBuffers,
+ hasSideTextures = this.hasSideTextures(),
+ applyLighting = this.activeAttributes.applyLighting,
+ numFloatsPerVertex = 3 + (hasSideTextures ? 2 : 0) + (applyLighting ? 3 : 0),
+ numBytesPerVertex = 4 * numFloatsPerVertex,
+ vboId, opacity, color, textureBound, sidesBuffer, numSides;
+
+ numSides = 0;
+ for (var b = 0; b < currentData.boundaryPoints.length; b++) { // for each boundary}
+ numSides += currentData.boundaryPoints[b].length / 6 - 1; // 6 floats per boundary point: top + bottom
+ }
+
+ if (!currentData.sidesVboCacheKey) {
+ currentData.sidesVboCacheKey = dc.gpuResourceCache.generateCacheKey();
+ }
+
+ vboId = dc.gpuResourceCache.resourceForKey(currentData.sidesVboCacheKey);
+ if (!vboId || refreshBuffers) {
+ sidesBuffer = this.makeSidesBuffer(numSides);
+ currentData.numSideVertices = sidesBuffer.length / numFloatsPerVertex;
+
+ if (!vboId) {
+ vboId = gl.createBuffer();
+ }
+
+ dc.gpuResourceCache.putResource(currentData.sidesVboCacheKey, vboId, sidesBuffer.length * 4);
+ gl.bindBuffer(gl.ARRAY_BUFFER, vboId);
+ gl.bufferData(gl.ARRAY_BUFFER, sidesBuffer, gl.STATIC_DRAW);
+ dc.frameStatistics.incrementVboLoadCount(1);
+ } else {
+ gl.bindBuffer(gl.ARRAY_BUFFER, vboId);
+ }
+
+ color = this.activeAttributes.interiorColor;
+ // Disable writing the shape's fragments to the depth buffer when the interior is semi-transparent.
+ gl.depthMask(color.alpha * this.layer.opacity >= 1 || dc.pickingMode);
+ program.loadColor(gl, dc.pickingMode ? pickColor : color);
+ program.loadOpacity(gl, dc.pickingMode ? 1 : this.layer.opacity);
+
+ if (hasSideTextures && !dc.pickingMode) {
+ if (applyLighting) {
+ program.loadApplyLighting(gl, true);
+ gl.enableVertexAttribArray(program.normalVectorLocation);
+ } else {
+ program.loadApplyLighting(gl, false);
+ }
+
+ // Step through the sides buffer rendering each side independently but from the same buffer.
+ for (var side = 0; side < numSides; side++) {
+ var sideImageSource = this.sideImageSource(side),
+ sideTexture = dc.gpuResourceCache.resourceForKey(sideImageSource),
+ coordByteOffset = side * 6 * numBytesPerVertex; // 6 vertices (2 triangles) per side
+
+ if (sideImageSource && !sideTexture) {
+ sideTexture = dc.gpuResourceCache.retrieveTexture(dc.currentGlContext, sideImageSource);
+ }
+
+ textureBound = sideTexture && sideTexture.bind(dc);
+ if (textureBound) {
+ gl.enableVertexAttribArray(program.vertexTexCoordLocation);
+ gl.vertexAttribPointer(program.vertexTexCoordLocation, 2, gl.FLOAT, false, numBytesPerVertex,
+ coordByteOffset + 12);
+
+ this.scratchMatrix.setToIdentity();
+ this.scratchMatrix.multiplyByTextureTransform(sideTexture);
+
+ program.loadTextureEnabled(gl, true);
+ program.loadTextureUnit(gl, gl.TEXTURE0);
+ program.loadTextureMatrix(gl, this.scratchMatrix);
+ } else {
+ program.loadTextureEnabled(gl, false);
+ gl.disableVertexAttribArray(program.vertexTexCoordLocation);
+ }
+
+ if (applyLighting) {
+ gl.vertexAttribPointer(program.normalVectorLocation, 3, gl.FLOAT, false, numBytesPerVertex,
+ coordByteOffset + 20);
+ }
+
+ gl.vertexAttribPointer(program.vertexPointLocation, 3, gl.FLOAT, false, numBytesPerVertex,
+ coordByteOffset);
+ gl.drawArrays(gl.TRIANGLES, 0, 6); // 6 vertices per side
+ }
+ } else {
+ program.loadTextureEnabled(gl, false);
+
+ if (applyLighting && !dc.pickingMode) {
+ program.loadApplyLighting(gl, true);
+ gl.enableVertexAttribArray(program.normalVectorLocation);
+ gl.vertexAttribPointer(program.normalVectorLocation, 3, gl.FLOAT, false, numBytesPerVertex,
+ numBytesPerVertex - 12);
+ } else {
+ program.loadApplyLighting(gl, false);
+ }
+
+ gl.vertexAttribPointer(program.vertexPointLocation, 3, gl.FLOAT, false, numBytesPerVertex, 0);
+ gl.drawArrays(gl.TRIANGLES, 0, currentData.numSideVertices);
+ }
+};
+
+Polygon.prototype.makeSidesBuffer = function (numSides) {
+ var currentData = this.currentData,
+ hasSideTextures = this.hasSideTextures(),
+ applyLighting = this.activeAttributes.applyLighting,
+ numFloatsPerVertex = 3 + (hasSideTextures ? 2 : 0) + (applyLighting ? 3 : 0),
+ sidesBuffer, sidesBufferIndex, numBufferFloats, v0, v1, v2, v3, t0, t1, t2, t3;
+
+ numBufferFloats = numSides * 2 * 3 * numFloatsPerVertex; // 2 triangles per side, 3 vertices per triangle
+ sidesBuffer = new Float32Array(numBufferFloats);
+ sidesBufferIndex = 0;
+
+ v0 = new Vec3(0, 0, 0);
+ v1 = new Vec3(0, 0, 0);
+ v2 = new Vec3(0, 0, 0);
+ v3 = new Vec3(0, 0, 0);
+
+ if (hasSideTextures) {
+ t0 = new Vec2(0, 1);
+ t1 = new Vec2(0, 0);
+ t2 = new Vec2(1, 1);
+ t3 = new Vec2(1, 0);
+ } else {
+ t0 = t1 = t2 = t3 = null;
+ }
+
+ for (var b = 0; b < currentData.boundaryPoints.length; b++) { // for each boundary}
+ var boundaryPoints = currentData.boundaryPoints[b],
+ sideNormal;
+
+ for (var i = 0; i < boundaryPoints.length - 6; i += 6) {
+ v0[0] = boundaryPoints[i];
+ v0[1] = boundaryPoints[i + 1];
+ v0[2] = boundaryPoints[i + 2];
+
+ v1[0] = boundaryPoints[i + 3];
+ v1[1] = boundaryPoints[i + 4];
+ v1[2] = boundaryPoints[i + 5];
+
+ v2[0] = boundaryPoints[i + 6];
+ v2[1] = boundaryPoints[i + 7];
+ v2[2] = boundaryPoints[i + 8];
+
+ v3[0] = boundaryPoints[i + 9];
+ v3[1] = boundaryPoints[i + 10];
+ v3[2] = boundaryPoints[i + 11];
+
+ sideNormal = applyLighting ? Vec3.computeTriangleNormal(v0, v1, v2) : null;
+
+ // First triangle.
+ this.addVertexToBuffer(v0, t0, sideNormal, sidesBuffer, sidesBufferIndex);
+ sidesBufferIndex += numFloatsPerVertex;
+
+ this.addVertexToBuffer(v1, t1, sideNormal, sidesBuffer, sidesBufferIndex);
+ sidesBufferIndex += numFloatsPerVertex;
+
+ this.addVertexToBuffer(v2, t2, sideNormal, sidesBuffer, sidesBufferIndex);
+ sidesBufferIndex += numFloatsPerVertex;
+
+ // Second triangle.
+ this.addVertexToBuffer(v1, t1, sideNormal, sidesBuffer, sidesBufferIndex);
+ sidesBufferIndex += numFloatsPerVertex;
+
+ this.addVertexToBuffer(v3, t3, sideNormal, sidesBuffer, sidesBufferIndex);
+ sidesBufferIndex += numFloatsPerVertex;
+
+ this.addVertexToBuffer(v2, t2, sideNormal, sidesBuffer, sidesBufferIndex);
+ sidesBufferIndex += numFloatsPerVertex;
+ }
+ }
+
+ return sidesBuffer;
+};
+
+Polygon.prototype.addVertexToBuffer = function (v, texCoord, normal, buffer, bufferIndex) {
+ buffer[bufferIndex++] = v[0];
+ buffer[bufferIndex++] = v[1];
+ buffer[bufferIndex++] = v[2];
+
+ if (texCoord) {
+ buffer[bufferIndex++] = texCoord[0];
+ buffer[bufferIndex++] = texCoord[1];
+ }
+
+ if (normal) {
+ buffer[bufferIndex++] = normal[0];
+ buffer[bufferIndex++] = normal[1];
+ buffer[bufferIndex] = normal[2];
+ }
+};
+
+Polygon.prototype.drawOutline = function (dc, pickColor) {
+ var gl = dc.currentGlContext,
+ program = dc.currentProgram,
+ currentData = this.currentData,
+ refreshBuffers = currentData.refreshBuffers,
+ numBoundaryPoints, vboId, opacity, color, stride, nPts, textureBound;
+
+ program.loadTextureEnabled(gl, false);
+ program.loadApplyLighting(gl, false);
+
+ if (this.hasCapTexture()) {
+ gl.disableVertexAttribArray(program.vertexTexCoordLocation); // we're not texturing the outline
+ }
+
+ if (this.activeAttributes.applyLighting) {
+ gl.disableVertexAttribArray(program.normalVectorLocation); // we're not lighting the outline
+ }
+
+ if (!currentData.boundaryVboCacheKeys) {
+ this.currentData.boundaryVboCacheKeys = [];
+ }
+
+ // Make the outline stand out from the interior.
+ this.applyMvpMatrixForOutline(dc);
+
+ program.loadTextureEnabled(gl, false);
+ gl.disableVertexAttribArray(program.vertexTexCoordLocation);
+
+ for (var b = 0; b < currentData.boundaryPoints.length; b++) { // for each boundary}
+ numBoundaryPoints = currentData.boundaryPoints[b].length / 3;
+
+ if (!currentData.boundaryVboCacheKeys[b]) {
+ currentData.boundaryVboCacheKeys[b] = dc.gpuResourceCache.generateCacheKey();
+ }
+
+ vboId = dc.gpuResourceCache.resourceForKey(currentData.boundaryVboCacheKeys[b]);
+ if (!vboId) {
+ vboId = gl.createBuffer();
+ dc.gpuResourceCache.putResource(currentData.boundaryVboCacheKeys[b], vboId, numBoundaryPoints * 12);
+ refreshBuffers = true;
+ }
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, vboId);
+ if (refreshBuffers) {
+ gl.bufferData(gl.ARRAY_BUFFER, currentData.boundaryPoints[b], gl.STATIC_DRAW);
+ dc.frameStatistics.incrementVboLoadCount(1);
+ }
+
+ color = this.activeAttributes.outlineColor;
+ // Disable writing the shape's fragments to the depth buffer when the interior is semi-transparent.
+ gl.depthMask(color.alpha * this.layer.opacity >= 1 || dc.pickingMode);
+ program.loadColor(gl, dc.pickingMode ? pickColor : color);
+ program.loadOpacity(gl, dc.pickingMode ? 1 : this.layer.opacity);
+
+ gl.lineWidth(this.activeAttributes.outlineWidth);
+
+ if (this._extrude) {
+ stride = 24;
+ nPts = numBoundaryPoints / 2;
+ } else {
+ stride = 12;
+ nPts = numBoundaryPoints;
+ }
+
+ gl.vertexAttribPointer(program.vertexPointLocation, 3, gl.FLOAT, false, stride, 0);
+ gl.drawArrays(gl.LINE_STRIP, 0, nPts);
+
+ if (this.mustDrawVerticals(dc)) {
+ gl.vertexAttribPointer(program.vertexPointLocation, 3, gl.FLOAT, false, 0, 0);
+ gl.drawArrays(gl.LINES, 0, numBoundaryPoints - 2);
+ }
+ }
+};
+
+// Overridden from AbstractShape base class.
+Polygon.prototype.beginDrawing = function (dc) {
+ var gl = dc.currentGlContext;
+
+ if (this.activeAttributes.drawInterior) {
+ gl.disable(gl.CULL_FACE);
+ }
+
+ dc.findAndBindProgram(BasicTextureProgram);
+ gl.enableVertexAttribArray(dc.currentProgram.vertexPointLocation);
+
+ var applyLighting = !dc.pickMode && this.activeAttributes.applyLighting;
+ if (applyLighting) {
+ dc.currentProgram.loadModelviewInverse(gl, dc.modelviewNormalTransform);
+ }
+};
+
+// Overridden from AbstractShape base class.
+Polygon.prototype.endDrawing = function (dc) {
+ var gl = dc.currentGlContext;
+
+ gl.disableVertexAttribArray(dc.currentProgram.vertexPointLocation);
+ gl.disableVertexAttribArray(dc.currentProgram.normalVectorLocation);
+ gl.depthMask(true);
+ gl.lineWidth(1);
+ gl.enable(gl.CULL_FACE);
+};
+
+export default Polygon;
diff --git a/web/test/WebWorldWind/src/shapes/ScreenImage.js b/web/test/WebWorldWind/src/shapes/ScreenImage.js
new file mode 100644
index 00000000..e058e8dd
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/ScreenImage.js
@@ -0,0 +1,374 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports ScreenImage
+ */
+import ArgumentError from '../error/ArgumentError';
+import BasicTextureProgram from '../shaders/BasicTextureProgram';
+import Color from '../util/Color';
+import ImageSource from '../util/ImageSource';
+import Logger from '../util/Logger';
+import Matrix from '../geom/Matrix';
+import Offset from '../util/Offset';
+import PickedObject from '../pick/PickedObject';
+import Renderable from '../render/Renderable';
+import Vec3 from '../geom/Vec3';
+import WWMath from '../util/WWMath';
+
+
+/**
+ * Constructs a screen image.
+ * @alias ScreenImage
+ * @constructor
+ * @augments Renderable
+ * @classdesc Displays an image at a specified screen location in the WorldWindow.
+ * The image location is specified by an offset, which causes the image to maintain its relative position
+ * when the window size changes.
+ * @param {Offset} screenOffset The offset indicating the image's placement on the screen.
+ * Use [the image offset property]{@link ScreenImage#imageOffset} to position the image relative to the
+ * specified screen offset.
+ * @param {String|ImageSource} imageSource The source of the image to display.
+ * May be either a string identifying the URL of the image, or an {@link ImageSource} object identifying a
+ * dynamically created image.
+ * @throws {ArgumentError} If the specified screen offset or image source is null or undefined.
+ */
+function ScreenImage(screenOffset, imageSource) {
+ if (!screenOffset) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "ScreenImage", "constructor", "missingOffset"));
+ }
+
+ if (!imageSource) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "ScreenImage", "constructor", "missingImage"));
+ }
+
+ Renderable.call(this);
+
+ /**
+ * The offset indicating this screen image's placement on the screen.
+ * @type {Offset}
+ */
+ this.screenOffset = screenOffset;
+
+ // Documented with its property accessor below.
+ this._imageSource = imageSource;
+
+ /**
+ * The image color. When displayed, this shape's image is multiplied by this image color to achieve the
+ * final image color. The color white, the default, causes the image to be drawn in its native colors.
+ * @type {Color}
+ * @default White (1, 1, 1, 1)
+ */
+ this.imageColor = Color.WHITE;
+
+ /**
+ * Indicates the location within the image at which to align with the specified screen location.
+ * May be null, in which case the image's bottom-left corner is placed at the screen location.
+ * @type {Offset}
+ * @default 0.5, 0.5, both fractional (Centers the image on the screen location.)
+ */
+ this.imageOffset = new Offset(WorldWind.OFFSET_FRACTION, 0.5, WorldWind.OFFSET_FRACTION, 0.5);
+
+ /**
+ * Indicates the amount to scale the image.
+ * @type {Number}
+ * @default 1
+ */
+ this.imageScale = 1;
+
+ /**
+ * The amount of rotation to apply to the image, measured in degrees clockwise from the top of the window.
+ * @type {Number}
+ * @default 0
+ */
+ this.imageRotation = 0;
+
+ /**
+ * The amount of tilt to apply to the image, measured in degrees.
+ * @type {Number}
+ * @default 0
+ */
+ this.imageTilt = 0;
+
+ /**
+ * Indicates whether to draw this screen image.
+ * @type {Boolean}
+ * @default true
+ */
+ this.enabled = true;
+
+ /**
+ * This image's opacity. When this screen image is drawn, the actual opacity is the product of
+ * this opacity and the opacity of the layer containing this screen image.
+ * @type {Number}
+ */
+ this.opacity = 1;
+
+ /**
+ * Indicates the object to return as the userObject of this shape when picked. If null,
+ * then this shape is returned as the userObject.
+ * @type {Object}
+ * @default null
+ * @see [PickedObject.userObject]{@link PickedObject#userObject}
+ */
+ this.pickDelegate = null;
+
+ // Internal use only. Intentionally not documented.
+ this.activeTexture = null;
+
+ // Internal use only. Intentionally not documented.
+ this.imageTransform = Matrix.fromIdentity();
+
+ // Internal use only. Intentionally not documented.
+ this.texCoordMatrix = Matrix.fromIdentity();
+
+ // Internal use only. Intentionally not documented.
+ this.imageBounds = null;
+
+ // Internal use only. Intentionally not documented.
+ this.layer = null;
+}
+
+// Internal use only. Intentionally not documented.
+ScreenImage.matrix = Matrix.fromIdentity(); // scratch variable
+
+ScreenImage.prototype = Object.create(Renderable.prototype);
+
+Object.defineProperties(ScreenImage.prototype, {
+ /**
+ * The source of the image to display.
+ * May be either a string identifying the URL of the image, or an {@link ImageSource} object identifying a
+ * dynamically created image.
+ * @type {String|ImageSource}
+ * @default null
+ * @memberof ScreenImage.prototype
+ */
+ imageSource: {
+ get: function () {
+ return this._imageSource;
+ },
+ set: function (imageSource) {
+ if (!imageSource) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "ScreenImage", "imageSource",
+ "missingImage"));
+ }
+
+ this._imageSource = imageSource;
+ this.imageSourceWasUpdated = true;
+ }
+ }
+});
+
+/**
+ * Renders this screen image. This method is typically not called by applications but is called by
+ * {@link RenderableLayer} during rendering. For this shape this method creates and
+ * enques an ordered renderable with the draw context and does not actually draw the image.
+ * @param {DrawContext} dc The current draw context.
+ */
+ScreenImage.prototype.render = function (dc) {
+ if (!this.enabled) {
+ return;
+ }
+
+ if (!dc.accumulateOrderedRenderables) {
+ return;
+ }
+
+ // Create an ordered renderable, but don't create more than one per frame.
+ var orderedScreenImage = null;
+ if (this.lastFrameTime !== dc.timestamp) {
+ orderedScreenImage = this.makeOrderedRenderable(dc);
+ }
+
+ if (!orderedScreenImage) {
+ return;
+ }
+
+ if (!orderedScreenImage.isVisible(dc)) {
+ return;
+ }
+
+ orderedScreenImage.layer = dc.currentLayer;
+
+ this.lastFrameTime = dc.timestamp;
+ dc.addOrderedRenderable(orderedScreenImage);
+};
+
+/**
+ * Draws this shape as an ordered renderable. Applications do not call this function. It is called by
+ * [WorldWindow]{@link WorldWindow} during rendering.
+ * @param {DrawContext} dc The current draw context.
+ */
+ScreenImage.prototype.renderOrdered = function (dc) {
+ this.drawOrderedScreenImage(dc);
+
+ if (dc.pickingMode) {
+ var po = new PickedObject(this.pickColor.clone(), this.pickDelegate ? this.pickDelegate : this,
+ null, this.layer, false);
+ dc.resolvePick(po);
+ }
+};
+
+// Internal. Intentionally not documented.
+ScreenImage.prototype.makeOrderedRenderable = function (dc) {
+ var w, h, s, ws, hs,
+ iOffset, sOffset;
+
+ this.activeTexture = this.getActiveTexture(dc);
+ if (!this.activeTexture || this.imageSourceWasUpdated) {
+ this.activeTexture = dc.gpuResourceCache.retrieveTexture(dc.currentGlContext, this._imageSource);
+ if (!this.activeTexture) {
+ return null;
+ }
+ }
+
+ this.eyeDistance = 0;
+
+ // Compute the image's transform matrix and texture coordinate matrix according to its screen point, image size,
+ // image offset and image scale. The image offset is defined with its origin at the image's bottom-left corner and
+ // axes that extend up and to the right from the origin point.
+ w = this.activeTexture.imageWidth;
+ h = this.activeTexture.imageHeight;
+ s = this.imageScale;
+ iOffset = this.imageOffset.offsetForSize(w, h);
+ ws = dc.viewport.width;
+ hs = dc.viewport.height;
+ sOffset = this.screenOffset.offsetForSize(ws, hs);
+
+ this.imageTransform.setTranslation(
+ sOffset[0] - iOffset[0] * s,
+ sOffset[1] - iOffset[1] * s,
+ 0);
+
+ this.imageTransform.setScale(w * s, h * s, 1);
+
+ this.imageBounds = WWMath.boundingRectForUnitQuad(this.imageTransform);
+
+ return this;
+};
+
+ScreenImage.prototype.getActiveTexture = function (dc) {
+ return dc.gpuResourceCache.resourceForKey(this._imageSource);
+};
+
+// Internal. Intentionally not documented.
+ScreenImage.prototype.isVisible = function (dc) {
+ if (dc.pickingMode) {
+ return dc.pickRectangle && this.imageBounds.intersects(dc.pickRectangle);
+ } else {
+ return this.imageBounds.intersects(dc.viewport);
+ }
+};
+
+// Internal. Intentionally not documented.
+ScreenImage.prototype.drawOrderedScreenImage = function (dc) {
+ this.beginDrawing(dc);
+ try {
+ this.doDrawOrderedScreenImage(dc);
+ } finally {
+ this.endDrawing(dc);
+ }
+};
+
+// Internal. Intentionally not documented.
+ScreenImage.prototype.beginDrawing = function (dc) {
+ var gl = dc.currentGlContext,
+ program;
+
+ dc.findAndBindProgram(BasicTextureProgram);
+
+ // Configure GL to use the draw context's unit quad VBOs for both model coordinates and texture coordinates.
+ // Most browsers can share the same buffer for vertex and texture coordinates, but Internet Explorer requires
+ // that they be in separate buffers, so the code below uses the 3D buffer for vertex coords and the 2D
+ // buffer for texture coords.
+ program = dc.currentProgram;
+ gl.bindBuffer(gl.ARRAY_BUFFER, dc.unitQuadBuffer());
+ gl.vertexAttribPointer(program.vertexTexCoordLocation, 2, gl.FLOAT, false, 0, 0);
+ gl.enableVertexAttribArray(program.vertexPointLocation);
+ gl.enableVertexAttribArray(program.vertexTexCoordLocation);
+
+ // Tell the program which texture unit to use.
+ program.loadTextureUnit(gl, gl.TEXTURE0);
+ program.loadModulateColor(gl, dc.pickingMode);
+
+ // Turn off depth testing.
+ gl.disable(gl.DEPTH_TEST);
+};
+
+// Internal. Intentionally not documented.
+ScreenImage.prototype.endDrawing = function (dc) {
+ var gl = dc.currentGlContext,
+ program = dc.currentProgram;
+
+ // Clear the vertex attribute state.
+ gl.disableVertexAttribArray(program.vertexPointLocation);
+ gl.disableVertexAttribArray(program.vertexTexCoordLocation);
+
+ // Clear GL bindings.
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
+ gl.bindTexture(gl.TEXTURE_2D, null);
+
+ // Re-enable depth testing.
+ gl.enable(gl.DEPTH_TEST);
+};
+
+// Internal. Intentionally not documented.
+ScreenImage.prototype.doDrawOrderedScreenImage = function (dc) {
+ var gl = dc.currentGlContext,
+ program = dc.currentProgram;
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, dc.unitQuadBuffer3());
+ gl.vertexAttribPointer(program.vertexPointLocation, 3, gl.FLOAT, false, 0, 0);
+
+ // Compute and specify the MVP matrix.
+ ScreenImage.matrix.copy(dc.screenProjection);
+ ScreenImage.matrix.multiplyMatrix(this.imageTransform);
+
+ ScreenImage.matrix.multiplyByTranslation(0.5, 0.5, 0.5); // shift Z to prevent image clipping
+ ScreenImage.matrix.multiplyByRotation(1, 0, 0, this.imageTilt);
+ ScreenImage.matrix.multiplyByRotation(0, 0, 1, this.imageRotation);
+ ScreenImage.matrix.multiplyByTranslation(-0.5, -0.5, 0);
+
+ program.loadModelviewProjection(gl, ScreenImage.matrix);
+
+ // Enable texture for both normal display and for picking. If picking is enabled in the shader (set in
+ // beginDrawing() above) then the texture's alpha component is still needed in order to modulate the
+ // pick color to mask off transparent pixels.
+ program.loadTextureEnabled(gl, true);
+
+ // Set the pick color for picking or the color and opacity if not picking.
+ if (dc.pickingMode) {
+ this.pickColor = dc.uniquePickColor();
+ program.loadColor(gl, this.pickColor);
+ program.loadOpacity(gl, 1);
+ } else {
+ program.loadColor(gl, this.imageColor);
+ program.loadOpacity(gl, this.opacity * this.layer.opacity);
+ }
+
+ this.texCoordMatrix.setToIdentity();
+ this.texCoordMatrix.multiplyByTextureTransform(this.activeTexture);
+ program.loadTextureMatrix(gl, this.texCoordMatrix);
+
+ if (this.activeTexture.bind(dc)) { // returns false if active texture cannot be bound
+ // Draw the placemark's image quad.
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+ }
+};
+
+export default ScreenImage;
diff --git a/web/test/WebWorldWind/src/shapes/ScreenText.js b/web/test/WebWorldWind/src/shapes/ScreenText.js
new file mode 100644
index 00000000..1ad99959
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/ScreenText.js
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports ScreenText
+ */
+import ArgumentError from '../error/ArgumentError';
+import Logger from '../util/Logger';
+import Offset from '../util/Offset';
+import Text from '../shapes/Text';
+
+
+/**
+ * Constructs a screen text shape at a specified screen location.
+ * @alias ScreenText
+ * @constructor
+ * @augments Text
+ * @classdesc Represents a string of text displayed at a screen location.
+ *
+ * See also {@link GeographicText}.
+ *
+ * @param {Offset} screenOffset The offset indicating the text's placement on the screen.
+ * Use [TextAttributes.offset]{@link TextAttributes#offset} to position the text relative to the specified
+ * screen offset.
+ * @param {String} text The text to display.
+ * @throws {ArgumentError} If either the specified screen offset or text is null or undefined.
+ */
+function ScreenText(screenOffset, text) {
+ if (!screenOffset) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Text", "constructor", "missingOffset"));
+ }
+
+ Text.call(this, text);
+
+ /**
+ * The offset indicating this text's placement on the screen.
+ * The [TextAttributes.offset]{@link TextAttributes#offset} property indicates the relationship of the text
+ * string to this location.
+ * @type {Offset}
+ */
+ this.screenOffset = screenOffset;
+
+ /**
+ * Inherited from [Text]{@link Text#altitudeMode} but not utilized by screen text.
+ */
+ this.altitudeMode = null;
+}
+
+ScreenText.prototype = Object.create(Text.prototype);
+
+// Documented in superclass.
+ScreenText.prototype.render = function (dc) {
+ // Ensure that this text is drawn only once per frame.
+ if (this.lastFrameTime !== dc.timestamp) {
+ Text.prototype.render.call(this, dc);
+ }
+};
+
+// Documented in superclass.
+ScreenText.prototype.computeScreenPointAndEyeDistance = function (dc) {
+ var gl = dc.currentGlContext,
+ offset = this.screenOffset.offsetForSize(gl.drawingBufferWidth, gl.drawingBufferHeight);
+
+ this.screenPoint[0] = offset[0];
+ this.screenPoint[1] = offset[1];
+ this.screenPoint[2] = 0;
+
+ this.eyeDistance = 0;
+
+ return true;
+};
+
+export default ScreenText;
diff --git a/web/test/WebWorldWind/src/shapes/ShapeAttributes.js b/web/test/WebWorldWind/src/shapes/ShapeAttributes.js
new file mode 100644
index 00000000..732a2ea0
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/ShapeAttributes.js
@@ -0,0 +1,310 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports ShapeAttributes
+ */
+import Color from '../util/Color';
+import ImageSource from '../util/ImageSource';
+
+
+/**
+ * Constructs a shape attributes bundle, optionally specifying a prototype set of attributes. Not all shapes
+ * use all the properties in the bundle. See the documentation of a specific shape to determine the properties
+ * it does use.
+ * @alias ShapeAttributes
+ * @constructor
+ * @classdesc Holds attributes applied to WorldWind shapes.
+ * @param {ShapeAttributes} attributes An attribute bundle whose properties are used to initially populate
+ * the constructed attributes bundle. May be null, in which case the constructed attributes bundle is populated
+ * with default attributes.
+ */
+function ShapeAttributes(attributes) {
+
+ // All these are documented with their property accessors below.
+ this._drawInterior = attributes ? attributes._drawInterior : true;
+ this._drawOutline = attributes ? attributes._drawOutline : true;
+ this._enableLighting = attributes ? attributes._enableLighting : false;
+ this._interiorColor = attributes ? attributes._interiorColor.clone() : Color.WHITE.clone();
+ this._outlineColor = attributes ? attributes._outlineColor.clone() : Color.RED.clone();
+ this._outlineWidth = attributes ? attributes._outlineWidth : 1.0;
+ this._outlineStippleFactor = attributes ? attributes._outlineStippleFactor : 0;
+ this._outlineStipplePattern = attributes ? attributes._outlineStipplePattern : 0xF0F0;
+ this._imageSource = attributes ? attributes._imageSource : null;
+ this._depthTest = attributes ? attributes._depthTest : true;
+ this._drawVerticals = attributes ? attributes._drawVerticals : false;
+ this._applyLighting = attributes ? attributes._applyLighting : false;
+
+ /**
+ * Indicates whether this object's state key is invalid. Subclasses must set this value to true when their
+ * attributes change. The state key will be automatically computed the next time it's requested. This flag
+ * will be set to false when that occurs.
+ * @type {Boolean}
+ * @protected
+ */
+ this.stateKeyInvalid = true;
+}
+
+/**
+ * Computes the state key for this attributes object. Subclasses that define additional attributes must
+ * override this method, call it from that method, and append the state of their attributes to its
+ * return value.
+ * @returns {String} The state key for this object.
+ * @protected
+ */
+ShapeAttributes.prototype.computeStateKey = function () {
+ return "di " + this._drawInterior +
+ " do " + this._drawOutline +
+ " el " + this._enableLighting +
+ " ic " + this._interiorColor.toHexString(true) +
+ " oc " + this._outlineColor.toHexString(true) +
+ " ow " + this._outlineWidth +
+ " osf " + this._outlineStippleFactor +
+ " osp " + this._outlineStipplePattern +
+ " is " + (this._imageSource ?
+ this.imageSource instanceof ImageSource ? this.imageSource.key : this.imageSource : "null") +
+ " dt " + this._depthTest +
+ " dv " + this._drawVerticals +
+ " li " + this._applyLighting;
+};
+
+Object.defineProperties(ShapeAttributes.prototype, {
+ /**
+ * A string identifying the state of this attributes object. The string encodes the current values of all
+ * this object's properties. It's typically used to validate cached representations of shapes associated
+ * with this attributes object.
+ * @type {String}
+ * @readonly
+ * @memberof ShapeAttributes.prototype
+ */
+ stateKey: {
+ get: function () {
+ if (this.stateKeyInvalid) {
+ this._stateKey = this.computeStateKey();
+ this.stateKeyInvalid = false;
+ }
+ return this._stateKey;
+ }
+ },
+
+ /**
+ * Indicates whether the interior of the associated shape is drawn.
+ * @type {Boolean}
+ * @default true
+ * @memberof ShapeAttributes.prototype
+ */
+ drawInterior: {
+ get: function () {
+ return this._drawInterior;
+ },
+ set: function (value) {
+ this._drawInterior = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates whether the outline of the associated shape is drawn
+ * @type {Boolean}
+ * @default true
+ * @memberof ShapeAttributes.prototype
+ */
+ drawOutline: {
+ get: function () {
+ return this._drawOutline;
+ },
+ set: function (value) {
+ this._drawOutline = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates whether lighting is applied to the associated shape.
+ * @type {Boolean}
+ * @default false
+ * @memberof ShapeAttributes.prototype
+ */
+ enableLighting: {
+ get: function () {
+ return this._enableLighting;
+ },
+ set: function (value) {
+ this._enableLighting = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates the associated shape's interior color and opacity.
+ * @type {Color}
+ * @default Opaque white (red = 1, green = 1, blue = 1, alpha = 1)
+ * @memberof ShapeAttributes.prototype
+ */
+ interiorColor: {
+ get: function () {
+ return this._interiorColor;
+ },
+ set: function (value) {
+ this._interiorColor = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates the associated shape's outline color and opacity.
+ * @type {Color}
+ * @default Opaque red (red = 1, green = 0, blue = 0, alpha = 1)
+ * @memberof ShapeAttributes.prototype
+ */
+ outlineColor: {
+ get: function () {
+ return this._outlineColor;
+ },
+ set: function (value) {
+ this._outlineColor = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates the associated shape's outline width.
+ * @type {Number}
+ * @default 1.0
+ * @memberof ShapeAttributes.prototype
+ */
+ outlineWidth: {
+ get: function () {
+ return this._outlineWidth;
+ },
+ set: function (value) {
+ this._outlineWidth = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates the associated shape's outline stipple pattern. Specifies a number whose lower 16 bits
+ * define a pattern of which pixels in the outline are rendered and which are suppressed. Each bit
+ * corresponds to a pixel in the shape's outline, and the pattern repeats after every n*16 pixels, where
+ * n is the [stipple factor]{@link ShapeAttributes#outlineStippleFactor}. For example, if the outline
+ * stipple factor is 3, each bit in the stipple pattern is repeated three times before using the next bit.
+ *
+ * To disable outline stippling, either specify a stipple factor of 0 or specify a stipple pattern of
+ * all 1 bits, i.e., 0xFFFF.
+ * @type {Number}
+ * @default 0xF0F0
+ * @memberof ShapeAttributes.prototype
+ */
+ outlineStipplePattern: {
+ get: function () {
+ return this._outlineStipplePattern;
+ },
+ set: function (value) {
+ this._outlineStipplePattern = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates the associated shape's outline stipple factor. Specifies the number of times each bit in the
+ * outline stipple pattern is repeated before the next bit is used. For example, if the outline stipple
+ * factor is 3, each bit is repeated three times before using the next bit. The specified factor must be
+ * either 0 or an integer greater than 0. A stipple factor of 0 indicates no stippling.
+ * @type {Number}
+ * @default 0
+ * @memberof ShapeAttributes.prototype
+ */
+ outlineStippleFactor: {
+ get: function () {
+ return this._outlineStippleFactor;
+ },
+ set: function (value) {
+ this._outlineStippleFactor = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates the associated shape's image source. May be null, in which case no image is
+ * applied to the shape.
+ * @type {String|ImageSource}
+ * @memberof ShapeAttributes.prototype
+ * @default null
+ */
+ imageSource: {
+ get: function () {
+ return this._imageSource;
+ },
+ set: function (value) {
+ this._imageSource = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates whether the shape should be depth-tested against other objects in the scene. If true,
+ * the shape may be occluded by terrain and other objects in certain viewing situations. If false,
+ * the shape will not be occluded by terrain and other objects.
+ * @type {Boolean}
+ * @default true
+ * @memberof ShapeAttributes.prototype
+ */
+ depthTest: {
+ get: function () {
+ return this._depthTest;
+ },
+ set: function (value) {
+ this._depthTest = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates whether this shape should draw vertical lines extending from its specified positions to the
+ * ground.
+ * @type {Boolean}
+ * @default false
+ * @memberof ShapeAttributes.prototype
+ */
+ drawVerticals: {
+ get: function () {
+ return this._drawVerticals;
+ },
+ set: function (value) {
+ this._drawVerticals = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates whether lighting is applied to the shape.
+ * @type {Boolean}
+ * @default false
+ * @memberof ShapeAttributes.prototype
+ */
+ applyLighting: {
+ get: function () {
+ return this._applyLighting;
+ },
+ set: function (value) {
+ this._applyLighting = value;
+ this.stateKeyInvalid = true;
+ }
+ }
+});
+
+export default ShapeAttributes;
diff --git a/web/test/WebWorldWind/src/shapes/SurfaceCircle.js b/web/test/WebWorldWind/src/shapes/SurfaceCircle.js
new file mode 100644
index 00000000..84ad3ce5
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/SurfaceCircle.js
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports SurfaceCircle
+ */
+import ArgumentError from '../error/ArgumentError';
+import Location from '../geom/Location';
+import Logger from '../util/Logger';
+import ShapeAttributes from '../shapes/ShapeAttributes';
+import SurfaceShape from '../shapes/SurfaceShape';
+
+
+/**
+ * Constructs a surface circle with a specified center and radius and an optional attributes bundle.
+ * @alias SurfaceCircle
+ * @constructor
+ * @augments SurfaceShape
+ * @classdesc Represents a circle draped over the terrain surface.
+ *
+ * SurfaceCircle uses the following attributes from its associated shape attributes bundle:
+ *
+ * SurfaceEllipse uses the following attributes from its associated shape attributes bundle:
+ *
+ * SurfacePolygon uses the following attributes from its associated shape attributes bundle:
+ *
+ * SurfacePolyline uses the following attributes from its associated shape attributes bundle:
+ *
+ * SurfaceRectangle uses the following attributes from its associated shape attributes bundle:
+ *
+ * SurfaceSector uses the following attributes from its associated shape attributes bundle:
+ *
+ * Surface shapes other than SurfacePolyline {@link SurfacePolyline} have an interior and an outline and utilize
+ * the corresponding attributes in their associated ShapeAttributes {@link ShapeAttributes}. They do not
+ * utilize image-related attributes.
+ *
+ * @param {ShapeAttributes} attributes The attributes to apply to this shape. May be null, in which case
+ * attributes must be set directly before the shape is drawn.
+ */
+function SurfaceShape(attributes) {
+
+ Renderable.call(this);
+
+ // All these are documented with their property accessors below.
+ this._displayName = "Surface Shape";
+ this._attributes = attributes ? attributes : new ShapeAttributes(null);
+ this._highlightAttributes = null;
+ this._highlighted = false;
+ this._enabled = true;
+ this._pathType = WorldWind.GREAT_CIRCLE;
+ this._maximumNumEdgeIntervals = SurfaceShape.DEFAULT_NUM_EDGE_INTERVALS;
+ this._polarThrottle = SurfaceShape.DEFAULT_POLAR_THROTTLE;
+ this._boundingSector = null;
+
+ /**
+ * Indicates the object to return as the owner of this shape when picked.
+ * @type {Object}
+ * @default null
+ */
+ this.pickDelegate = null;
+
+ /*
+ * The bounding sectors for this tile, which may be needed for crossing the dateline.
+ * @type {Sector[]}
+ * @protected
+ */
+ this._boundingSectors = [];
+
+ /*
+ * The raw collection of locations defining this shape and are explicitly specified by the client of this class.
+ * @type {Location[]}
+ * @protected
+ */
+ this._locations = null;
+
+ /*
+ * Boundaries that are either the user specified locations or locations that are algorithmically generated.
+ * @type {Location[]}
+ * @protected
+ */
+ this._boundaries = null;
+
+ /*
+ * The collection of locations that describes a closed curve which can be filled.
+ * @type {Location[][]}
+ * @protected
+ */
+ this._interiorGeometry = null;
+
+ /*
+ * The collection of locations that describe the outline of the shape.
+ * @type {Location[][]}
+ * @protected
+ */
+ this._outlineGeometry = null;
+
+ /*
+ * Internal use only.
+ * Inhibit the filling of the interior. This is to be used ONLY by polylines.
+ * @type {Boolean}
+ * @protected
+ */
+ this._isInteriorInhibited = false;
+
+ /*
+ * Indicates whether this object's state key is invalid. Subclasses must set this value to true when their
+ * attributes change. The state key will be automatically computed the next time it's requested. This flag
+ * will be set to false when that occurs.
+ * @type {Boolean}
+ * @protected
+ */
+ this.stateKeyInvalid = true;
+
+ // Internal use only. Intentionally not documented.
+ this._attributesStateKey = null;
+
+ // Internal use only. Intentionally not documented.
+ this.boundariesArePrepared = false;
+
+ // Internal use only. Intentionally not documented.
+ this.layer = null;
+
+ // Internal use only. Intentionally not documented.
+ this.pickColor = null;
+
+ //the split contours returned from polygon splitter
+ this.contours = [];
+ this.containsPole = false;
+ this.crossesAntiMeridian = false;
+
+ /**
+ * Indicates how long to use terrain-specific shape data before regenerating it, in milliseconds. A value
+ * of zero specifies that shape data should be regenerated every frame. While this causes the shape to
+ * adapt more frequently to the terrain, it decreases performance.
+ * @type {Number}
+ * @default 2000 (milliseconds)
+ */
+ this.expirationInterval = 2000;
+
+ // Internal use only. Intentionally not documented.
+ // Holds the per-globe data
+ this.shapeDataCache = new MemoryCache(3, 2);
+
+ // Internal use only. Intentionally not documented.
+ // The shape-data-cache data that is for the currently active globe.
+ this.currentData = null;
+}
+
+SurfaceShape.prototype = Object.create(Renderable.prototype);
+
+Object.defineProperties(SurfaceShape.prototype, {
+ stateKey: {
+ /**
+ * A hash key of the total visible external state of the surface shape.
+ * @memberof SurfaceShape.prototype
+ * @type {String}
+ */
+ get: function () {
+ // If we don't have a state key for the shape attributes, consider this state key to be invalid.
+ if (!this._attributesStateKey) {
+ // Update the state key for the appropriate attributes for future
+ if (this._highlighted) {
+ if (this._highlightAttributes) {
+ this._attributesStateKey = this._highlightAttributes.stateKey;
+ }
+ } else {
+ if (this._attributes) {
+ this._attributesStateKey = this._attributes.stateKey;
+ }
+ }
+
+ // If we now actually have a state key for the attributes, it was previously invalid.
+ if (this._attributesStateKey) {
+ this.stateKeyInvalid = true;
+ }
+ } else {
+ // Detect a change in the appropriate attributes.
+ var currentAttributesStateKey = null;
+
+ if (this._highlighted) {
+ // If there are highlight attributes associated with this shape, ...
+ if (this._highlightAttributes) {
+ currentAttributesStateKey = this._highlightAttributes.stateKey;
+ }
+ } else {
+ if (this._attributes) {
+ currentAttributesStateKey = this._attributes.stateKey;
+ }
+ }
+
+ // If the attributes state key changed, ...
+ if (currentAttributesStateKey != this._attributesStateKey) {
+ this._attributesStateKey = currentAttributesStateKey;
+ this.stateKeyInvalid = true;
+ }
+ }
+
+ if (this.stateKeyInvalid) {
+ this._stateKey = this.computeStateKey();
+ }
+
+ return this._stateKey;
+ }
+ },
+
+ /**
+ * The shape's display name and label text.
+ * @memberof SurfaceShape.prototype
+ * @type {String}
+ * @default Surface Shape
+ */
+ displayName: {
+ get: function () {
+ return this._displayName;
+ },
+ set: function (value) {
+ this.stateKeyInvalid = true;
+ this._displayName = value;
+ }
+ },
+
+ /**
+ * The shape's attributes. If null and this shape is not highlighted, this shape is not drawn.
+ * @memberof SurfaceShape.prototype
+ * @type {ShapeAttributes}
+ * @default see [ShapeAttributes]{@link ShapeAttributes}
+ */
+ attributes: {
+ get: function () {
+ return this._attributes;
+ },
+ set: function (value) {
+ this.stateKeyInvalid = true;
+ this._attributes = value;
+ this._attributesStateKey = value.stateKey;
+ }
+ },
+
+ /**
+ * The attributes used when this shape's highlighted flag is true. If null and the
+ * highlighted flag is true, this shape's normal attributes are used. If they, too, are null, this
+ * shape is not drawn.
+ * @memberof SurfaceShape.prototype
+ * @type {ShapeAttributes}
+ * @default null
+ */
+ highlightAttributes: {
+ get: function () {
+ return this._highlightAttributes;
+ },
+ set: function (value) {
+ this.stateKeyInvalid = true;
+ this._highlightAttributes = value;
+ }
+ },
+
+ /**
+ * Indicates whether this shape displays with its highlight attributes rather than its normal attributes.
+ * @memberof SurfaceShape.prototype
+ * @type {Boolean}
+ * @default false
+ */
+ highlighted: {
+ get: function () {
+ return this._highlighted;
+ },
+ set: function (value) {
+ this.stateKeyInvalid = true;
+ this._highlighted = value;
+ }
+ },
+
+ /**
+ * Indicates whether this shape is drawn.
+ * @memberof SurfaceShape.prototype
+ * @type {Boolean}
+ * @default true
+ */
+ enabled: {
+ get: function () {
+ return this._enabled;
+ },
+ set: function (value) {
+ this.stateKeyInvalid = true;
+ this._enabled = value;
+ }
+ },
+
+ /**
+ * The path type to used to interpolate between locations on this shape. Recognized values are:
+ *
+ * Altitudes within the mesh's positions are interpreted according to the mesh's altitude mode, which
+ * can be one of the following:
+ *
+ * Meshes have separate attributes for normal display and highlighted display. They use the interior and
+ * outline attributes of {@link ShapeAttributes}. If those attributes identify an image, that image is
+ * applied to the mesh. Texture coordinates for the image may be specified, but if not specified the full
+ * image is stretched over the full mesh. If texture coordinates are specified, there must be one texture
+ * coordinate for each vertex in the mesh.
+ *
+ * @param {Position[]} positions An array containing the mesh vertices.
+ * There must be no more than 65536 positions. Use [split]{@link TriangleMesh#split} to subdivide large meshes
+ * into smaller ones that fit this limit.
+ * @param {Number[]} indices An array of integers identifying the positions of each mesh triangle.
+ * Each sequence of three indices defines one triangle in the mesh. The indices identify the index of the
+ * position in the associated positions array. The indices for each triangle should be in counter-clockwise
+ * order to identify the triangles as front-facing.
+ * @param {ShapeAttributes} attributes The attributes to associate with this mesh. May be null, in which case
+ * default attributes are associated.
+ *
+ * @throws {ArgumentError} If the specified positions array is null, empty or undefined, the number of indices
+ * is less than 3 or too many positions are specified (limit is 65536).
+ */
+function TriangleMesh(positions, indices, attributes) {
+ if (!positions) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "TriangleMesh", "constructor", "missingPositions"));
+ }
+
+ if (positions.length < 1) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "TriangleMesh", "constructor", "missingPositions"));
+ }
+
+ // Check for size limit, which is the max number of available indices for a 16-bit unsigned int.
+ if (positions.length > 65536) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "TriangleMesh", "constructor",
+ "Too many positions. Must be fewer than 65537. Use TriangleMesh.split to split the shape."));
+ }
+
+ if (!indices) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "TriangleMesh", "constructor",
+ "Indices array is null or undefined"));
+ }
+
+ if (indices.length < 3) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "TriangleMesh", "constructor", "Too few indices."));
+ }
+
+ AbstractMesh.call(this, attributes);
+
+ // Private. Documentation is with the defined property below and the constructor description above.
+ this._positions = positions;
+
+ // Private. Documentation is with the defined property below and the constructor description above.
+ this._indices = indices;
+
+ this.referencePosition = this._positions[0];
+}
+
+TriangleMesh.prototype = Object.create(AbstractMesh.prototype);
+
+Object.defineProperties(TriangleMesh.prototype, {
+ /**
+ * This mesh's positions.
+ *
+ * @type {Position[]}
+ * @memberof TriangleMesh.prototype
+ */
+ positions: {
+ get: function () {
+ return this._positions;
+ },
+ set: function (positions) {
+ if (!positions) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "TriangleMesh", "positions", "missingPositions"));
+ }
+
+ this._positions = positions;
+ this.referencePosition = this._positions[0];
+ this.reset();
+ }
+ },
+
+ /**
+ * The mesh indices, an array of integers identifying the indexes of each triangle. Each index in this
+ * array identifies the index of the corresponding position in the [positions]{@link TriangleMesh#positions}
+ * array. Each group of three indices in this array identifies the positions of one triangle.
+ *
+ *
+ * @type {Number[]}
+ * @memberof TriangleMesh.prototype
+ */
+ indices: {
+ get: function () {
+ return this._indices;
+ },
+ set: function (indices) {
+ if (!indices) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "TriangleMesh", "indices",
+ "Indices array is null or undefined"));
+ }
+
+ this._indices = indices;
+ this.meshIndices = null;
+ this.reset();
+ }
+ },
+
+ /**
+ * The mesh outline indices, an array of integers identifying the positions in the outline. Each index in
+ * this array identifies the index of the corresponding position in the
+ * [positions]{@link TriangleMesh#positions} array. The collection of these positions forms the outline
+ * of this mesh. May be null, in which case no outline is drawn.
+ *
+ * @type {Number[]}
+ * @default null
+ * @memberof TriangleMesh.prototype
+ */
+ outlineIndices: {
+ get: function () {
+ return this._outlineIndices;
+ },
+ set: function (indices) {
+ this._outlineIndices = indices;
+ this.meshOutlineIndices = null;
+ this.reset();
+ }
+ },
+
+ /**
+ * This mesh's texture coordinates if this mesh is textured. A texture coordinate must be
+ * provided for each mesh position. Each texture coordinate is a {@link Vec2} containing the s and t
+ * coordinates, in that order. If no texture coordinates are specified then texture is not applied to
+ * this mesh.
+ * @type {Vec2[]}
+ * @default null
+ * @memberof TriangleMesh.prototype
+ */
+ textureCoordinates: {
+ get: function () {
+ return this._textureCoordinates;
+ },
+ set: function (coords) {
+
+ if (coords && coords.length != this._positions.length) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "TriangleMesh", "textureCoordinates",
+ "Number of texture coordinates is inconsistent with the currently specified positions."));
+ }
+
+ this._textureCoordinates = coords;
+ this.reset();
+ this.texCoords = null;
+ }
+ }
+});
+
+// Overridden from AbstractShape base class.
+TriangleMesh.prototype.createSurfaceShape = function () {
+ if (this._outlineIndices) {
+ var boundaries = [];
+
+ for (var i = 0; i < this._outlineIndices.length; i++) {
+ boundaries.push(this._positions[this._outlineIndices[i]]);
+ }
+
+ return new SurfacePolygon(boundaries, null);
+ } else {
+ return null;
+ }
+
+};
+
+// Overridden from AbstractShape base class.
+TriangleMesh.prototype.computeMeshPoints = function (dc, currentData) {
+ var eyeDistSquared = Number.MAX_VALUE,
+ eyePoint = dc.eyePoint,
+ meshPoints = new Float32Array(this._positions.length * 3),
+ pt = new Vec3(0, 0, 0),
+ k = 0,
+ pos, dSquared;
+
+ for (var i = 0; i < this._positions.length; i++) {
+ pos = this._positions[i];
+
+ dc.surfacePointForMode(pos.latitude, pos.longitude, pos.altitude * this._altitudeScale,
+ this.altitudeMode, pt);
+
+ dSquared = pt.distanceToSquared(eyePoint);
+ if (dSquared < eyeDistSquared) {
+ eyeDistSquared = dSquared;
+ }
+
+ pt.subtract(this.currentData.referencePoint);
+
+ meshPoints[k++] = pt[0];
+ meshPoints[k++] = pt[1];
+ meshPoints[k++] = pt[2];
+ }
+
+ currentData.eyeDistance = Math.sqrt(eyeDistSquared);
+
+ return meshPoints;
+};
+
+// Overridden from AbstractShape base class.
+TriangleMesh.prototype.computeTexCoords = function () {
+
+ if (!this._textureCoordinates) {
+ return null;
+ } else {
+ // Capture the texture coordinates to a single array parallel to the mesh points array.
+ var texCoords = new Float32Array(2 * this._textureCoordinates.length),
+ k = 0;
+
+ for (var i = 0, len = this._textureCoordinates.length; i < len; i++) {
+ var texCoord = this._textureCoordinates[i];
+
+ texCoords[k++] = texCoord[0];
+ texCoords[k++] = texCoord[1];
+ }
+
+ return texCoords;
+ }
+};
+
+// Overridden from AbstractShape base class.
+TriangleMesh.prototype.computeMeshIndices = function () {
+ var meshIndices = new Uint16Array(this._indices.length);
+
+ for (var i = 0, len = this._indices.length; i < len; i++) {
+ meshIndices[i] = this._indices[i];
+ }
+
+ return meshIndices;
+};
+
+// Overridden from AbstractShape base class.
+TriangleMesh.prototype.computeOutlineIndices = function () {
+ if (!this._outlineIndices) {
+ return null;
+ } else {
+ var meshOutlineIndices = new Uint16Array(this._outlineIndices.length);
+
+ for (var i = 0; i < this._outlineIndices.length; i++) {
+ meshOutlineIndices[i] = this._outlineIndices[i];
+ }
+
+ return meshOutlineIndices;
+ }
+};
+
+/**
+ * Splits a triangle mesh into several meshes, each of which contains fewer than 65536 positions.
+ * @param {Position[]} positions An array containing the mesh vertices.
+ * @param {Number[]} indices An array of integers identifying the positions of each mesh triangle.
+ * Each sequence of three indices defines one triangle in the mesh. The indices identify the index of the
+ * position in the associated positions array.
+ * @param {Vec2[]} textureCoords The mesh's texture coordinates.
+ * @param {Number[]} outlineIndices The mesh's outline indices.
+ * @returns {Object[]} An array of objects, each of which defines one subdivision of the full mesh. Each object
+ * in the array has the properties of the same name as the input arguments.
+ */
+TriangleMesh.split = function (positions, indices, textureCoords, outlineIndices) {
+ var splitPositions = [],
+ splitTexCoords = [],
+ splitIndices = [],
+ indexMap = [],
+ result = [],
+ originalIndex, mappedIndex;
+
+ for (var i = 0; i <= indices.length; i++) {
+ if (i === indices.length || splitPositions.length > 65533 && splitIndices.length % 3 === 0) {
+ if (splitPositions.length > 0) {
+ var shape = {
+ positions: splitPositions,
+ indices: splitIndices
+ };
+
+ if (textureCoords) {
+ shape.textureCoords = splitTexCoords;
+ }
+
+ if (outlineIndices) {
+ var splitOutline = [];
+ for (var j = 0; j < outlineIndices.length; j++) {
+ originalIndex = outlineIndices[j];
+ mappedIndex = indexMap[originalIndex];
+ if (mappedIndex) {
+ splitOutline.push(indexMap[outlineIndices[j]]);
+ }
+ }
+
+ shape.outlineIndices = splitOutline;
+ }
+
+ result.push(shape);
+ }
+
+ if (i === indices.length) {
+ break;
+ }
+
+ splitPositions = [];
+ splitIndices = [];
+ indexMap = [];
+ }
+
+ originalIndex = indices[i];
+ mappedIndex = indexMap[originalIndex];
+
+ if (!mappedIndex) {
+ mappedIndex = splitPositions.length;
+ indexMap[originalIndex] = mappedIndex;
+
+ splitPositions.push(positions[originalIndex]);
+
+ if (textureCoords) {
+ splitTexCoords.push(textureCoords[originalIndex]);
+ }
+ }
+
+ splitIndices.push(mappedIndex);
+ }
+
+ return result;
+};
+
+export default TriangleMesh;
diff --git a/web/test/WebWorldWind/src/util/AbsentResourceList.js b/web/test/WebWorldWind/src/util/AbsentResourceList.js
new file mode 100644
index 00000000..5031c104
--- /dev/null
+++ b/web/test/WebWorldWind/src/util/AbsentResourceList.js
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports AbsentResourceList
+ */
+
+
+
+/**
+ * Constructs an absent resource list.
+ * @alias AbsentResourceList
+ * @constructor
+ * @classdesc Provides a collection to keep track of resources whose retrieval failed and when retrieval
+ * may be tried again. Applications typically do not use this class directly.
+ * @param {Number} maxTrys The number of attempts to make before the resource is marked as absent.
+ * @param {Number} minCheckInterval The amount of time to wait between attempts, in milliseconds.
+ * @constructor
+ */
+function AbsentResourceList(maxTrys, minCheckInterval) {
+
+ /**
+ * The number of attempts to make before the resource is marked as absent.
+ * @type {Number}
+ */
+ this.maxTrys = maxTrys;
+
+ /**
+ * The amount of time to wait before each attempt.
+ * @type {Number}
+ */
+ this.minCheckInterval = minCheckInterval;
+
+ /**
+ * The amount of time, in milliseconds, beyond which retrieval attempts should again be allowed.
+ * When this time has elapsed from the most recent failed attempt the number of trys attempted is
+ * reset to 0. This prevents the resource from being permanently blocked.
+ * @type {number}
+ * @default 60,000 milliseconds (one minute)
+ */
+ this.tryAgainInterval = 60e3; // 60 seconds
+
+ this.possiblyAbsent = {};
+}
+
+/**
+ * Indicates whether a specified resource is marked as absent.
+ * @param {String} resourceId The resource identifier.
+ * @returns {Boolean} true if the resource is marked as absent, otherwise false.
+ */
+AbsentResourceList.prototype.isResourceAbsent = function (resourceId) {
+ var entry = this.possiblyAbsent[resourceId];
+
+ if (!entry) {
+ return false;
+ }
+
+ if (entry.permanent) {
+ return true;
+ }
+
+ var timeSinceLastMark = Date.now() - entry.timeOfLastMark;
+
+ if (timeSinceLastMark > this.tryAgainInterval) {
+ delete this.possiblyAbsent[resourceId];
+ return false;
+ }
+
+ return timeSinceLastMark < this.minCheckInterval || entry.numTrys > this.maxTrys;
+};
+
+/**
+ * Marks a resource attempt as having failed. This increments the number-of-tries counter and sets the time
+ * of the last attempt. When this method has been called [this.maxTrys]{@link AbsentResourceList#maxTrys}
+ * times the resource is marked as absent until this absent resource list's
+ * [try-again-interval]{@link AbsentResourceList#tryAgainInterval} is reached.
+ * @param {String} resourceId The resource identifier.
+ */
+AbsentResourceList.prototype.markResourceAbsent = function (resourceId) {
+ var entry = this.possiblyAbsent[resourceId];
+
+ if (!entry) {
+ entry = {
+ timeOfLastMark: Date.now(),
+ numTrys: 0
+ };
+ this.possiblyAbsent[resourceId] = entry;
+ }
+
+ entry.numTrys = entry.numTrys + 1;
+ entry.timeOfLastMark = Date.now();
+};
+
+/**
+ * Marks a resource attempt as having failed permanently. No attempt will ever again be made to retrieve
+ * the resource.
+ * @param {String} resourceId The resource identifier.
+ */
+AbsentResourceList.prototype.markResourceAbsentPermanently = function (resourceId) {
+ var entry = this.possiblyAbsent[resourceId];
+
+ if (!entry) {
+ entry = {
+ timeOfLastMark: Date.now(),
+ numTrys: 0
+ };
+ this.possiblyAbsent[resourceId] = entry;
+ }
+
+ entry.numTrys = entry.numTrys + 1;
+ entry.timeOfLastMark = Date.now();
+ entry.permanent = true;
+};
+
+/**
+ * Removes the specified resource from this absent resource list. Call this method when retrieval attempts
+ * succeed.
+ * @param {String} resourceId The resource identifier.
+ */
+AbsentResourceList.prototype.unmarkResourceAbsent = function (resourceId) {
+ var entry = this.possiblyAbsent[resourceId];
+
+ if (entry) {
+ delete this.possiblyAbsent[resourceId];
+ }
+};
+
+export default AbsentResourceList;
diff --git a/web/test/WebWorldWind/src/util/Color.js b/web/test/WebWorldWind/src/util/Color.js
new file mode 100644
index 00000000..be539eb9
--- /dev/null
+++ b/web/test/WebWorldWind/src/util/Color.js
@@ -0,0 +1,353 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports Color
+ */
+import Logger from '../util/Logger';
+
+
+/**
+ * Constructs a color from red, green, blue and alpha values.
+ * @alias Color
+ * @constructor
+ * @classdesc Represents a red, green, blue, alpha, color.
+ * @param {Number} red The red component, a number between 0 and 1.
+ * @param {Number} green The green component, a number between 0 and 1.
+ * @param {Number} blue The blue component, a number between 0 and 1.
+ * @param {Number} alpha The alpha component, a number between 0 and 1.
+ */
+function Color(red, green, blue, alpha) {
+
+ /**
+ * This color's red component, a number between 0 and 1.
+ * @type {Number}
+ */
+ this.red = red;
+
+ /**
+ * This color's green component, a number between 0 and 1.
+ * @type {Number}
+ */
+ this.green = green;
+
+ /**
+ * This color's blue component, a number between 0 and 1.
+ * @type {Number}
+ */
+ this.blue = blue;
+
+ /**
+ * This color's alpha component, a number between 0 and 1.
+ * @type {Number}
+ */
+ this.alpha = alpha;
+}
+
+/**
+ * The color white.
+ * @type {Color}
+ * @constant
+ */
+Color.WHITE = new Color(1, 1, 1, 1);
+
+/**
+ * The color black.
+ * @type {Color}
+ * @constant
+ */
+Color.BLACK = new Color(0, 0, 0, 1);
+
+/**
+ * The color red.
+ * @type {Color}
+ * @constant
+ */
+Color.RED = new Color(1, 0, 0, 1);
+
+/**
+ * The color green.
+ * @type {Color}
+ * @constant
+ */
+Color.GREEN = new Color(0, 1, 0, 1);
+
+/**
+ * The color blue.
+ * @type {Color}
+ * @constant
+ */
+Color.BLUE = new Color(0, 0, 1, 1);
+
+/**
+ * The color cyan.
+ * @type {Color}
+ * @constant
+ */
+Color.CYAN = new Color(0, 1, 1, 1);
+
+/**
+ * The color yellow.
+ * @type {Color}
+ * @constant
+ */
+Color.YELLOW = new Color(1, 1, 0, 1);
+
+/**
+ * The color magenta.
+ * @type {Color}
+ * @constant
+ */
+Color.MAGENTA = new Color(1, 0, 1, 1);
+
+/**
+ * A light gray (75% white).
+ * @type {Color}
+ */
+Color.LIGHT_GRAY = new Color(0.75, 0.75, 0.75, 1);
+
+/**
+ * A medium gray (50% white).
+ * @type {Color}
+ */
+Color.MEDIUM_GRAY = new Color(0.5, 0.5, 0.5, 1);
+
+/**
+ * A dark gray (25% white).
+ * @type {Color}
+ */
+Color.DARK_GRAY = new Color(0.25, 0.25, 0.25, 1);
+
+/**
+ * A transparent color.
+ * @type {Color}
+ */
+Color.TRANSPARENT = new Color(0, 0, 0, 0);
+
+/**
+ * Assigns the components of this color.
+ * @param {Number} red The red component, a number between 0 and 1.
+ * @param {Number} green The green component, a number between 0 and 1.
+ * @param {Number} blue The blue component, a number between 0 and 1.
+ * @param {Number} alpha The alpha component, a number between 0 and 1.
+ * @returns {Color} This color with the specified components assigned.
+ */
+Color.prototype.set = function (red, green, blue, alpha) {
+ this.red = red;
+ this.green = green;
+ this.blue = blue;
+ this.alpha = alpha;
+
+ return this;
+};
+
+/**
+ * Copies the components of a specified color to this color.
+ * @param {Color} color The color to copy.
+ * @returns {Color} This color set to the red, green, blue and alpha values of the specified color.
+ * @throws {ArgumentError} If the specified color is null or undefined.
+ */
+Color.prototype.copy = function (color) {
+ if (!color) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Color", "copy", "missingColor"));
+ }
+
+ this.red = color.red;
+ this.green = color.green;
+ this.blue = color.blue;
+ this.alpha = color.alpha;
+
+ return this;
+};
+
+/**
+ * Create a copy of this color.
+ * @returns {Color} A new instance containing the color components of this color.
+ */
+Color.prototype.clone = function () {
+ return new Color(this.red, this.green, this.blue, this.alpha);
+};
+
+/**
+ * Returns this color's components premultiplied by this color's alpha component.
+ * @param {Float32Array} array A pre-allocated array in which to return the color components.
+ * @returns {Float32Array} This colors premultiplied components as an array, in the order RGBA.
+ */
+Color.prototype.premultipliedComponents = function (array) {
+ var a = this.alpha;
+
+ array[0] = this.red * a;
+ array[1] = this.green * a;
+ array[2] = this.blue * a;
+ array[3] = a;
+
+ return array;
+};
+
+/**
+ * Construct a color from an array of color components expressed as byte values.
+ * @param {Uint8Array} bytes A four-element array containing the red, green, blue and alpha color
+ * components each in the range [0, 255];
+ * @returns {Color} The constructed color.
+ */
+Color.colorFromByteArray = function (bytes) {
+ return new Color(bytes[0] / 255, bytes[1] / 255, bytes[2] / 255, bytes[3] / 255);
+};
+
+/**
+ * Construct a color from specified color components expressed as byte values.
+ * @param {number} redByte The red component in the range [0, 255].
+ * @param {number} greenByte The green component in the range [0, 255].
+ * @param {number} blueByte The blue component in the range [0, 255].
+ * @param {number} alphaByte The alpha component in the range [0, 255].
+ * @returns {Color} The constructed color.
+ */
+Color.colorFromBytes = function (redByte, greenByte, blueByte, alphaByte) {
+ return new Color(redByte / 255, greenByte / 255, blueByte / 255, alphaByte / 255);
+};
+
+Color.colorFromHex = function (color) {
+ var red = parseInt(color.substring(0, 2), 16);
+ var green = parseInt(color.substring(2, 4), 16);
+ var blue = parseInt(color.substring(4, 6), 16);
+ var alpha = parseInt(color.substring(6, 8), 16);
+ return Color.colorFromBytes(red, green, blue, alpha);
+};
+
+Color.colorFromKmlHex = function (color) {
+ var alpha = parseInt(color.substring(0, 2), 16);
+ var blue = parseInt(color.substring(2, 4), 16);
+ var green = parseInt(color.substring(4, 6), 16);
+ var red = parseInt(color.substring(6, 8), 16);
+ return Color.colorFromBytes(red, green, blue, alpha);
+};
+
+/**
+ * Computes and sets this color to the next higher RBG color. If the color overflows, this color is set to
+ * (1 / 255, 0, 0, *), where * indicates the current alpha value.
+ * @returns {Color} This color, set to the next possible color.
+ */
+Color.prototype.nextColor = function () {
+ var rb = Math.round(this.red * 255),
+ gb = Math.round(this.green * 255),
+ bb = Math.round(this.blue * 255);
+
+ if (rb < 255) {
+ this.red = (rb + 1) / 255;
+ } else if (gb < 255) {
+ this.red = 0;
+ this.green = (gb + 1) / 255;
+ } else if (bb < 255) {
+ this.red = 0;
+ this.green = 0;
+ this.blue = (bb + 1) / 255;
+ } else {
+ this.red = 1 / 255;
+ this.green = 0;
+ this.blue = 0;
+ }
+
+ return this;
+};
+
+/**
+ * Indicates whether this color is equal to a specified color after converting the floating-point component
+ * values of each color to byte values.
+ * @param {Color} color The color to test,
+ * @returns {Boolean} true if the colors are equal, otherwise false.
+ */
+Color.prototype.equals = function (color) {
+ var rbA = Math.round(this.red * 255),
+ gbA = Math.round(this.green * 255),
+ bbA = Math.round(this.blue * 255),
+ abA = Math.round(this.alpha * 255),
+ rbB = Math.round(color.red * 255),
+ gbB = Math.round(color.green * 255),
+ bbB = Math.round(color.blue * 255),
+ abB = Math.round(color.alpha * 255);
+
+ return rbA === rbB && gbA === gbB && bbA === bbB && abA === abB;
+};
+
+/**
+ * Indicates whether this color is equal to another color expressed as an array of bytes.
+ * @param {Uint8Array} bytes The red, green, blue and alpha color components.
+ * @returns {Boolean} true if the colors are equal, otherwise false.
+ */
+Color.prototype.equalsBytes = function (bytes) {
+ var rb = Math.round(this.red * 255),
+ gb = Math.round(this.green * 255),
+ bb = Math.round(this.blue * 255),
+ ab = Math.round(this.alpha * 255);
+
+ return rb === bytes[0] && gb === bytes[1] && bb === bytes[2] && ab === bytes[3];
+};
+
+/**
+ * Returns a string representation of this color, indicating the byte values corresponding to this color's
+ * floating-point component values.
+ * @returns {String}
+ */
+Color.prototype.toByteString = function () {
+ var rb = Math.round(this.red * 255),
+ gb = Math.round(this.green * 255),
+ bb = Math.round(this.blue * 255),
+ ab = Math.round(this.alpha * 255);
+
+ return "(" + rb + "," + gb + "," + bb + "," + ab + ")";
+};
+
+/**
+ * Create a hex color string that CSS can use. Optionally, inhibit capturing alpha,
+ * because some uses reject a four-component color specification.
+ * @param {Boolean} isUsingAlpha Enable the use of an alpha component.
+ * @returns {string} A color string suitable for CSS.
+ * @deprecated since version 0.10.0, use toCssColorString for valid CSS color strings
+ */
+Color.prototype.toHexString = function (isUsingAlpha) {
+ // Use Math.ceil() to get 0.75 to map to 0xc0. This is important if the display is dithering.
+ var redHex = Math.ceil(this.red * 255).toString(16),
+ greenHex = Math.ceil(this.green * 255).toString(16),
+ blueHex = Math.ceil(this.blue * 255).toString(16),
+ alphaHex = Math.ceil(this.alpha * 255).toString(16);
+
+ var result = "#";
+ result += redHex.length < 2 ? '0' + redHex : redHex;
+ result += greenHex.length < 2 ? '0' + greenHex : greenHex;
+ result += blueHex.length < 2 ? '0' + blueHex : blueHex;
+ if (isUsingAlpha) {
+ result += alphaHex.length < 2 ? '0' + alphaHex : alphaHex;
+ }
+
+ return result;
+};
+
+/**
+ * Create a rgba color string that conforms to CSS Color Module Level 3 specification.
+ * @returns {string} A color string suitable for CSS.
+ */
+Color.prototype.toCssColorString = function () {
+ var red = Math.round(this.red * 255),
+ green = Math.round(this.green * 255),
+ blue = Math.round(this.blue * 255);
+
+ // Per the CSS Color Module Level 3 specification, alpha is expressed as floating point value between 0 - 1
+ return 'rgba(' + red + ', ' + green + ', ' + blue + ', ' + this.alpha + ')';
+};
+
+export default Color;
+
diff --git a/web/test/WebWorldWind/src/util/Font.js b/web/test/WebWorldWind/src/util/Font.js
new file mode 100644
index 00000000..7284da02
--- /dev/null
+++ b/web/test/WebWorldWind/src/util/Font.js
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports Font
+ */
+import ArgumentError from '../error/ArgumentError';
+import Color from '../util/Color';
+import Logger from '../util/Logger';
+
+
+/**
+ * Construct a font descriptor. See the individual attribute descriptions below for possible parameter values.
+ * @param {Number} size The size of font.
+ * @param {String} style The style of the font.
+ * @param {String} variant The variant of the font.
+ * @param {String} weight The weight of the font.
+ * @param {String} family The family of the font.
+ * @param {String} horizontalAlignment The vertical alignment of the font.
+ * @alias Font
+ * @constructor
+ * @classdesc Holds attributes controlling the style, size and other attributes of {@link Text} shapes and
+ * the textual features of {@link Placemark} and other shapes. The values used for these attributes are those
+ * defined by the [CSS Font property]{@link http://www.w3schools.com/cssref/pr_font_font.asp}.
+ */
+function Font(size, style, variant, weight, family, horizontalAlignment) {
+ /*
+ * All properties of Font are intended to be private and must be accessed via public getters and setters.
+ */
+
+ if (!size) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Font", "constructor",
+ "missingSize"));
+ }
+ else if (size <= 0) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Font", "constructor",
+ "invalidSize"));
+ }
+ else {
+ this._size = size;
+ }
+
+ this.style = style || "normal";
+ this.variant = variant || "normal";
+ this.weight = weight || "normal";
+ this.family = family || "sans-serif";
+ this.horizontalAlignment = horizontalAlignment || "center";
+}
+
+Object.defineProperties(Font.prototype, {
+ /**
+ * The font size.
+ * @memberof Font.prototype
+ * @type Number
+ */
+ size: {
+ get: function () {
+ return this._size;
+ },
+ set: function (value) {
+ this._fontString = null;
+ this._size = value;
+ }
+ },
+ /**
+ * The font style.
+ * See [CSS font-style]{@link http://www.w3schools.com/cssref/pr_font_font-style.asp} for defined values.
+ * @memberof Font.prototype
+ * @type {String}
+ * @default "normal"
+ */
+ style: {
+ get: function () {
+ return this._style;
+ },
+ set: function (value) {
+ this._fontString = null;
+ this._style = value;
+ }
+ },
+ /**
+ * The font variant.
+ * See [CSS font-variant]{@link http://www.w3schools.com/cssref/pr_font_font-variant.asp} for defined values.
+ * @memberof Font.prototype
+ * @type {String}
+ * @default "normal"
+ */
+ variant: {
+ get: function () {
+ return this._variant;
+ },
+ set: function (value) {
+ this._fontString = null;
+ this._variant = value;
+ }
+ },
+ /**
+ * The font weight.
+ * See [CSS font-weight]{@link http://www.w3schools.com/cssref/pr_font_weight.asp} for defined values.
+ * @memberof Font.prototype
+ * @type {String}
+ * @default "normal"
+ */
+ weight: {
+ get: function () {
+ return this._weight;
+ },
+ set: function (value) {
+ this._fontString = null;
+ this._weight = value;
+ }
+ },
+ /**
+ * The font family.
+ * See [CSS font-family]{@link http://www.w3schools.com/cssref/pr_font_font-family.asp} for defined values.
+ * @memberof Font.prototype
+ * @type {String}
+ * @default "sans-serif"
+ */
+ family: {
+ get: function () {
+ return this._family;
+ },
+ set: function (value) {
+ this._fontString = null;
+ this._family = value;
+ }
+ },
+ /**
+ * The horizontal alignment of the font.
+ * Recognized values are "left", "center" and "right".
+ * @memberof Font.prototype
+ * @type {String}
+ * @default "center"
+ */
+ horizontalAlignment: {
+ get: function () {
+ return this._horizontalAlignment;
+ },
+ set: function (value) {
+ this._toString = null;
+ this._horizontalAlignment = value;
+ }
+ },
+
+ /**
+ * A string representing this font's style, weight, size and family properties, suitable for
+ * passing directly to a 2D canvas context.
+ * @memberof Font.prototype
+ */
+ fontString: {
+ get: function () {
+ if (!this._fontString) {
+ this._fontString =
+ this._style + " " +
+ this.variant + " " +
+ this._weight + " " +
+ this._size.toString() + "px " +
+ this._family;
+ }
+ return this._fontString;
+ }
+ }
+});
+
+/**
+ * Returns a string representation of this object.
+ * @returns {String} A string representation of this object.
+ */
+Font.prototype.toString = function () {
+ if (!this._toString || !this._fontString) {
+ this._toString = this.fontString + " " + this.horizontalAlignment;
+ }
+ return this._toString;
+};
+
+export default Font;
diff --git a/web/test/WebWorldWind/src/util/FrameStatistics.js b/web/test/WebWorldWind/src/util/FrameStatistics.js
new file mode 100644
index 00000000..93b30b67
--- /dev/null
+++ b/web/test/WebWorldWind/src/util/FrameStatistics.js
@@ -0,0 +1,220 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports FrameStatistics
+ */
+
+
+
+/**
+ * Constructs a performance statistics instance. This is performed internally by the {@link WorldWindow}.
+ * Applications do not construct instances of this class.
+ * @alias FrameStatistics
+ * @constructor
+ * @classdesc Captures performance statistics.
+ */
+function FrameStatistics() {
+
+ // Internal: intentionally not documented
+ this.frameCount = 0;
+
+ // Internal: intentionally not documented
+ this.frameTimeCumulative = 0;
+
+ // Internal: intentionally not documented
+ this.frameTimeBase = 0;
+
+ // Internal: intentionally not documented
+ this.frameTimeExtremes = [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY];
+
+ /**
+ * The number of milliseconds required to render the most recent frame.
+ * @type {Number}
+ */
+ this.frameTime = 0;
+
+ /**
+ * The number of milliseconds spent tessellating the terrain during the most recent frame.
+ * @type {Number}
+ */
+ this.tessellationTime = 0;
+
+ /**
+ * The number of milliseconds spent rendering the active layers during the most recent frame.
+ * @type {Number}
+ */
+ this.layerRenderingTime = 0;
+
+ /**
+ * The number of milliseconds spent rendering ordered renderables during the most recent frame.
+ * @type {Number}
+ */
+ this.orderedRenderingTime = 0;
+
+ /**
+ * The number of terrain tiles in the most recent frame.
+ * @type {Number}
+ */
+ this.terrainTileCount = 0;
+
+ /**
+ * The number of image tiles in the most recent frame.
+ * @type {Number}
+ */
+ this.imageTileCount = 0;
+
+ /**
+ * The number of terrain tile renderings. Since terrain tiles are generally rendered more than once per
+ * frame, this count will be greater than the number of terrain tiles created for the frame.
+ * @type {Number}
+ */
+ this.renderedTileCount = 0;
+
+ /**
+ * The number of calls to [Tile.update()]{@link Tile#update} during the most recent frame.
+ * @type {Number}
+ */
+ this.tileUpdateCount = 0;
+
+ /**
+ * The number of texture bind calls during the most recent frame.
+ * @type {Number}
+ */
+ this.textureLoadCount = 0;
+
+ /**
+ * The number of WebGL VBO loads during the most recent frame.
+ * @type {Number}
+ */
+ this.vboLoadCount = 0;
+
+ /**
+ * The average frame time over the most recent two seconds.
+ * @type {Number}
+ */
+ this.frameTimeAverage = 0;
+
+ /**
+ * The average frame rate over the most recent two seconds.
+ * @type {Number}
+ */
+ this.frameRateAverage = 0;
+
+ /**
+ * The minimum frame time over the most recent two seconds.
+ * @type {Number}
+ */
+ this.frameTimeMin = 0;
+
+ /**
+ * The maximum frame time over the most recent two seconds.
+ * @type {Number}
+ */
+ this.frameTimeMax = 0;
+}
+
+/**
+ * Initializes this frame statistics with initial values.
+ */
+FrameStatistics.prototype.beginFrame = function () {
+ this.frameTime = Date.now();
+ this.tessellationTime = 0;
+ this.layerRenderingTime = 0;
+ this.orderedRenderingTime = 0;
+ this.terrainTileCount = 0;
+ this.imageTileCount = 0;
+ this.renderedTileCount = 0;
+ this.tileUpdateCount = 0;
+ this.textureLoadCount = 0;
+ this.vboLoadCount = 0;
+
+ ++this.frameCount;
+};
+
+/**
+ * Computes the statistics for the most recent frame.
+ */
+FrameStatistics.prototype.endFrame = function () {
+ var now = Date.now();
+ this.frameTime = now - this.frameTime;
+ this.frameTimeCumulative += this.frameTime;
+ this.frameTimeExtremes[0] = Math.min(this.frameTimeExtremes[0], this.frameTime);
+ this.frameTimeExtremes[1] = Math.max(this.frameTimeExtremes[1], this.frameTime);
+
+ // Compute averages every 2 seconds.
+ if (now - this.frameTimeBase > 2000) {
+ this.frameTimeAverage = this.frameTimeCumulative / this.frameCount;
+ this.frameRateAverage = 1000 * this.frameCount / (now - this.frameTimeBase);
+ this.frameTimeMin = this.frameTimeExtremes[0];
+ this.frameTimeMax = this.frameTimeExtremes[1];
+ this.frameCount = 0;
+ this.frameTimeCumulative = 0;
+ this.frameTimeBase = now;
+ this.frameTimeExtremes = [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY];
+ //console.log(this.frameTimeAverage.toString() + ", " + this.frameRateAverage.toString());
+ }
+};
+
+/**
+ * Increments the rendered tile count.
+ * @param {Number} tileCount The amount to increment the counter.
+ */
+FrameStatistics.prototype.incrementRenderedTileCount = function (tileCount) {
+ this.renderedTileCount += tileCount;
+};
+
+/**
+ * Sets the terrain tile count.
+ * @param {Number} tileCount The amount to set the counter to.
+ */
+FrameStatistics.prototype.setTerrainTileCount = function (tileCount) {
+ this.terrainTileCount = tileCount;
+};
+
+/**
+ * Increments the image tile count.
+ * @param {Number} tileCount The amount to increment the counter.
+ */
+FrameStatistics.prototype.incrementImageTileCount = function (tileCount) {
+ this.imageTileCount = tileCount;
+};
+
+/**
+ * Increments the tile update count.
+ * @param {Number} count The amount to increment the counter.
+ */
+FrameStatistics.prototype.incrementTileUpdateCount = function (count) {
+ this.tileUpdateCount += count;
+};
+
+/**
+ * Increments the texture load count.
+ * @param {Number} count The amount to increment the counter.
+ */
+FrameStatistics.prototype.incrementTextureLoadCount = function (count) {
+ this.textureLoadCount += count;
+};
+
+/**
+ * Increments the VBO load count.
+ * @param {Number} count The amount to increment the counter.
+ */
+FrameStatistics.prototype.incrementVboLoadCount = function (count) {
+ this.vboLoadCount += count;
+};
+
+export default FrameStatistics;
diff --git a/web/test/WebWorldWind/src/util/GoToAnimator.js b/web/test/WebWorldWind/src/util/GoToAnimator.js
new file mode 100644
index 00000000..d0a13568
--- /dev/null
+++ b/web/test/WebWorldWind/src/util/GoToAnimator.js
@@ -0,0 +1,262 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports GoToAnimator
+ */
+import Location from '../geom/Location';
+import Logger from '../util/Logger';
+import Position from '../geom/Position';
+import Vec3 from '../geom/Vec3';
+
+/**
+ * Constructs a GoTo animator.
+ * @alias GoToAnimator
+ * @constructor
+ * @classdesc Incrementally and smoothly moves a {@link Navigator} to a specified position.
+ * @param {WorldWindow} worldWindow The WorldWindow in which to perform the animation.
+ * @throws {ArgumentError} If the specified WorldWindow is null or undefined.
+ */
+function GoToAnimator(worldWindow) {
+ if (!worldWindow) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "GoToAnimator", "constructor",
+ "missingWorldWindow"));
+ }
+
+ /**
+ * The WorldWindow associated with this animator.
+ * @type {WorldWindow}
+ * @readonly
+ */
+ this.wwd = worldWindow;
+
+ /**
+ * The frequency in milliseconds at which to animate the position change.
+ * @type {Number}
+ * @default 20
+ */
+ this.animationFrequency = 20;
+
+ /**
+ * The animation's duration, in milliseconds. When the distance is short, less than twice the viewport
+ * size, the travel time is reduced proportionally to the distance to travel. It therefore takes less
+ * time to move shorter distances.
+ * @type {Number}
+ * @default 3000
+ */
+ this.travelTime = 3000;
+
+ /**
+ * Indicates whether the current or most recent animation has been cancelled. Use the cancel() function
+ * to cancel an animation.
+ * @type {Boolean}
+ * @default false
+ * @readonly
+ */
+ this.cancelled = false;
+}
+
+// Stop the current animation.
+GoToAnimator.prototype.cancel = function () {
+ this.cancelled = true;
+};
+
+/**
+ * Moves the navigator to a specified location or position.
+ * @param {Location | Position} position The location or position to move the navigator to. If this
+ * argument contains an "altitude" property, as {@link Position} does, the end point of the navigation is
+ * at the specified altitude. Otherwise the end point is at the current altitude of the navigator.
+ * @param {Function} completionCallback If not null or undefined, specifies a function to call when the
+ * animation completes. The completion callback is called with a single argument, this animator.
+ * @throws {ArgumentError} If the specified location or position is null or undefined.
+ */
+GoToAnimator.prototype.goTo = function (position, completionCallback) {
+ if (!position) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "GoToAnimator", "goTo",
+ "missingPosition"));
+ }
+
+ this.completionCallback = completionCallback;
+
+ // Reset the cancellation flag.
+ this.cancelled = false;
+
+ // Capture the target position and determine its altitude.
+ this.targetPosition = new Position(position.latitude, position.longitude,
+ position.altitude || this.wwd.navigator.range);
+
+ // Capture the start position and start time.
+ this.startPosition = new Position(
+ this.wwd.navigator.lookAtLocation.latitude,
+ this.wwd.navigator.lookAtLocation.longitude,
+ this.wwd.navigator.range);
+ this.startTime = Date.now();
+
+ // Determination of the pan and range velocities requires the distance to be travelled.
+ var animationDuration = this.travelTime,
+ panDistance = Location.greatCircleDistance(this.startPosition, this.targetPosition),
+ rangeDistance;
+
+ // Determine how high we need to go to give the user context. The max altitude computed is approximately
+ // that needed to fit the start and end positions in the same viewport assuming a 45 degree field of view.
+ var pA = this.wwd.globe.computePointFromLocation(
+ this.startPosition.latitude, this.startPosition.longitude, new Vec3(0, 0, 0)),
+ pB = this.wwd.globe.computePointFromLocation(
+ this.targetPosition.latitude, this.targetPosition.longitude, new Vec3(0, 0, 0));
+ this.maxAltitude = pA.distanceTo(pB);
+
+ // Determine an approximate viewport size in radians in order to determine whether we actually change
+ // the range as we pan to the new location. We don't want to change the range if the distance between
+ // the start and target positions is small relative to the current viewport.
+ var viewportSize = this.wwd.pixelSizeAtDistance(this.startPosition.altitude)
+ * this.wwd.canvas.clientWidth / this.wwd.globe.equatorialRadius;
+
+ if (panDistance <= 2 * viewportSize) {
+ // Start and target positions are close, so don't back out.
+ this.maxAltitude = this.startPosition.altitude;
+ }
+
+ // We need to capture the time the max altitude is reached in order to begin decreasing the range
+ // midway through the animation. If we're already above the max altitude, then that time is now since
+ // we don't back out if the current altitude is above the computed max altitude.
+ this.maxAltitudeReachedTime = this.maxAltitude <= this.wwd.navigator.range ? Date.now() : null;
+
+ // Compute the total range to travel since we need that to compute the range velocity.
+ // Note that the range velocity and pan velocity are computed so that the respective animations, which
+ // operate independently, finish at the same time.
+ if (this.maxAltitude > this.startPosition.altitude) {
+ rangeDistance = Math.max(0, this.maxAltitude - this.startPosition.altitude);
+ rangeDistance += Math.abs(this.targetPosition.altitude - this.maxAltitude);
+ } else {
+ rangeDistance = Math.abs(this.targetPosition.altitude - this.startPosition.altitude);
+ }
+
+ // Determine which distance governs the animation duration.
+ var animationDistance = Math.max(panDistance, rangeDistance / this.wwd.globe.equatorialRadius);
+ if (animationDistance === 0) {
+ return; // current and target positions are the same
+ }
+
+ if (animationDistance < 2 * viewportSize) {
+ // Start and target positions are close, so reduce the travel time based on the
+ // distance to travel relative to the viewport size.
+ animationDuration = Math.min(animationDistance / viewportSize * this.travelTime, this.travelTime);
+ }
+
+ // Don't let the animation duration go to 0.
+ animationDuration = Math.max(1, animationDuration);
+
+ // Determine the pan velocity, in radians per millisecond.
+ this.panVelocity = panDistance / animationDuration;
+
+ // Determine the range velocity, in meters per millisecond.
+ this.rangeVelocity = rangeDistance / animationDuration; // meters per millisecond
+
+ // Set up the animation timer.
+ var thisAnimator = this;
+ var timerCallback = function () {
+ if (thisAnimator.cancelled) {
+ if (thisAnimator.completionCallback) {
+ thisAnimator.completionCallback(thisAnimator);
+ }
+ return;
+ }
+
+ if (thisAnimator.update()) {
+ setTimeout(timerCallback, thisAnimator.animationFrequency);
+ } else if (thisAnimator.completionCallback) {
+ thisAnimator.completionCallback(thisAnimator);
+ }
+ };
+ setTimeout(timerCallback, this.animationFrequency); // invoke it the first time
+};
+
+// Intentionally not documented.
+GoToAnimator.prototype.update = function () {
+ // This is the timer callback function. It invokes the range animator and the pan animator.
+
+ var currentPosition = new Position(
+ this.wwd.navigator.lookAtLocation.latitude,
+ this.wwd.navigator.lookAtLocation.longitude,
+ this.wwd.navigator.range);
+
+ var continueAnimation = this.updateRange(currentPosition);
+ continueAnimation = this.updateLocation(currentPosition) || continueAnimation;
+
+ this.wwd.redraw();
+
+ return continueAnimation;
+};
+
+// Intentionally not documented.
+GoToAnimator.prototype.updateRange = function (currentPosition) {
+ // This function animates the range.
+ var continueAnimation = false,
+ nextRange, elapsedTime;
+
+ // If we haven't reached the maximum altitude, then step-wise increase it. Otherwise step-wise change
+ // the range towards the target altitude.
+ if (!this.maxAltitudeReachedTime) {
+ elapsedTime = Date.now() - this.startTime;
+ nextRange = Math.min(this.startPosition.altitude + this.rangeVelocity * elapsedTime, this.maxAltitude);
+ // We're done if we get withing 1 meter of the desired range.
+ if (Math.abs(this.wwd.navigator.range - nextRange) < 1) {
+ this.maxAltitudeReachedTime = Date.now();
+ }
+ this.wwd.navigator.range = nextRange;
+ continueAnimation = true;
+ } else {
+ elapsedTime = Date.now() - this.maxAltitudeReachedTime;
+ if (this.maxAltitude > this.targetPosition.altitude) {
+ nextRange = this.maxAltitude - this.rangeVelocity * elapsedTime;
+ nextRange = Math.max(nextRange, this.targetPosition.altitude);
+ } else {
+ nextRange = this.maxAltitude + this.rangeVelocity * elapsedTime;
+ nextRange = Math.min(nextRange, this.targetPosition.altitude);
+ }
+ this.wwd.navigator.range = nextRange;
+ // We're done if we get withing 1 meter of the desired range.
+ continueAnimation = Math.abs(this.wwd.navigator.range - this.targetPosition.altitude) > 1;
+ }
+
+ return continueAnimation;
+};
+
+// Intentionally not documented.
+GoToAnimator.prototype.updateLocation = function (currentPosition) {
+ // This function animates the pan to the desired location.
+ var elapsedTime = Date.now() - this.startTime,
+ distanceTravelled = Location.greatCircleDistance(this.startPosition, currentPosition),
+ distanceRemaining = Location.greatCircleDistance(currentPosition, this.targetPosition),
+ azimuthToTarget = Location.greatCircleAzimuth(currentPosition, this.targetPosition),
+ distanceForNow = this.panVelocity * elapsedTime,
+ nextDistance = Math.min(distanceForNow - distanceTravelled, distanceRemaining),
+ nextLocation = Location.greatCircleLocation(currentPosition, azimuthToTarget, nextDistance,
+ new Location(0, 0)),
+ locationReached = false;
+
+ this.wwd.navigator.lookAtLocation.latitude = nextLocation.latitude;
+ this.wwd.navigator.lookAtLocation.longitude = nextLocation.longitude;
+
+ // We're done if we're within a meter of the desired location.
+ if (nextDistance < 1 / this.wwd.globe.equatorialRadius) {
+ locationReached = true;
+ }
+
+ return !locationReached;
+};
+
+export default GoToAnimator;
diff --git a/web/test/WebWorldWind/src/util/HashMap.js b/web/test/WebWorldWind/src/util/HashMap.js
new file mode 100644
index 00000000..346310d6
--- /dev/null
+++ b/web/test/WebWorldWind/src/util/HashMap.js
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports HashMap
+ */
+
+
+
+/**
+ * Constructs a hash map.
+ * @alias HashMap
+ * @constructor
+ */
+function HashMap() {
+ this._entries = Object.create(null);
+}
+
+/**
+ * Returns the stored value for this key or undefined
+ * @param{String | Number} key
+ * @returns the value for the specified key or undefined
+ */
+HashMap.prototype.get = function (key) {
+ return this._entries[key];
+};
+
+/**
+ * Stores a value for a specified key
+ * @param{String | Number} key
+ * @param value a value to store for the specified key
+ */
+HashMap.prototype.set = function (key, value) {
+ this._entries[key] = value;
+};
+
+/**
+ * Removes the value and key for a specified key
+ * @param{String | Number} key
+ */
+HashMap.prototype.remove = function (key) {
+ delete this._entries[key];
+};
+
+/**
+ * Indicates if the has map contains a key
+ * @param{String | Number} key
+ * @returns {Boolean}
+ */
+HashMap.prototype.contains = function (key) {
+ return key in this._entries;
+};
+
+/**
+ * Internal. Applications should call this function
+ * Creates a new HashMap with the same values as the original but increased indexes.
+ * The keys are used as indexes and are assumed to be natural numbers.
+ * Used by the PolygonSplitter.
+ * @param{HashMap} hashMap the hash map to re-index
+ * @param{Number} fromIndex the index from with to start reindexing
+ * @param{Number} amount the amount by which to increase the index
+ * @returns {HashMap} a new has map with re-indexed keys
+ */
+HashMap.reIndex = function (hashMap, fromIndex, amount) {
+ var newHashMap = new HashMap();
+ for (var key in hashMap._entries) {
+ var index = parseInt(key);
+ if (index >= fromIndex) {
+ index += amount;
+ }
+ var entry = hashMap.get(key);
+ entry.index = index;
+ newHashMap.set(index, entry);
+ }
+ return newHashMap;
+};
+
+export default HashMap;
diff --git a/web/test/WebWorldWind/src/util/ImageSource.js b/web/test/WebWorldWind/src/util/ImageSource.js
new file mode 100644
index 00000000..629c108c
--- /dev/null
+++ b/web/test/WebWorldWind/src/util/ImageSource.js
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports ImageSource
+ */
+import ArgumentError from '../error/ArgumentError';
+import Logger from '../util/Logger';
+
+
+/**
+ * Constructs an image source.
+ * @alias ImageSource
+ * @constructor
+ * @classdesc Holds an Image with an associated key that uniquely identifies that image. The key is
+ * automatically generated but may be reassigned after construction. Instances of this class are used to
+ * specify dynamically created image sources for {@link Placemark}, {@link SurfaceImage},
+ * {@link Polygon} textures and other shapes that display imagery.
+ * @param {Image} image The image for this image source.
+ * @throws {ArgumentError} If the specified image is null or undefined.
+ */
+function ImageSource(image) {
+ if (!image) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "ImageSource", "constructor",
+ "missingImage"));
+ }
+
+ /**
+ * This image source's image
+ * @type {Image}
+ * @readonly
+ */
+ this.image = image;
+
+ /**
+ * This image source's key. A unique key is automatically generated and assigned during construction.
+ * Applications may assign a different key after construction.
+ * @type {String}
+ * @default A unique string for this image source.
+ */
+ this.key = "ImageSource " + ++ImageSource.keyPool;
+}
+
+// Internal. Intentionally not documented.
+ImageSource.keyPool = 0; // source of unique ids
+
+export default ImageSource;
diff --git a/web/test/WebWorldWind/src/util/Insets.js b/web/test/WebWorldWind/src/util/Insets.js
new file mode 100644
index 00000000..51e00c17
--- /dev/null
+++ b/web/test/WebWorldWind/src/util/Insets.js
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import ArgumentError from '../error/ArgumentError';
+import Logger from '../util/Logger';
+
+
+/**
+ * Constructs an Insets object that is a representation of the borders of a container.
+ * It specifies the space that a container must leave at each of its edges.
+ * @alias Insets
+ * @param {Number} top The inset from the top.
+ * @param {Number} left The inset from the left.
+ * @param {Number} bottom The inset from the bottom.
+ * @param {Number} right The inset from the right.
+ * @constructor
+ */
+function Insets(top, left, bottom, right) {
+
+ if (arguments.length !== 4) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Insets", "constructor", "invalidArgumentCount"));
+ }
+
+ // These are all documented with their property accessors below.
+ this._top = top;
+ this._left = left;
+ this._bottom = bottom;
+ this._right = right;
+}
+
+/**
+ * Set top, left, bottom, and right to the specified values.
+ * @param {Number} top The inset from the top.
+ * @param {Number} left The inset from the left.
+ * @param {Number} bottom The inset from the bottom.
+ * @param {Number} right The inset from the right.
+ */
+Insets.prototype.set = function (top, left, bottom, right) {
+ this._top = top;
+ this._left = left;
+ this._bottom = bottom;
+ this._right = right;
+};
+
+/**
+ * Creates a new copy of this insets with identical property values.
+ * @returns {Insets} A new insets instance with its property values the same as this one's.
+ */
+Insets.prototype.clone = function () {
+ return new Insets(this._top, this._left, this._bottom, this._right);
+};
+
+/**
+ * Returns a string representation of this object.
+ * @returns {String} A string representation of this object.
+ */
+Insets.prototype.toString = function () {
+ return this._top + " " + this._left + " " + this._bottom + " " + this._right;
+};
+
+Object.defineProperties(Insets.prototype, {
+
+ /**
+ * Indicates the the inset from the top.
+ * @type {Number}
+ * @memberof Insets.prototype
+ */
+ top: {
+ get: function () {
+ return this._top;
+ },
+ set: function (value) {
+ this._top = value;
+ }
+ },
+
+ /**
+ * Indicates the the inset from the left.
+ * @type {Number}
+ * @memberof Insets.prototype
+ */
+ left: {
+ get: function () {
+ return this._left;
+ },
+ set: function (value) {
+ this._left = value;
+ }
+ },
+
+ /**
+ * Indicates the the inset from the bottom.
+ * @type {Number}
+ * @memberof Insets.prototype
+ */
+ bottom: {
+ get: function () {
+ return this._bottom;
+ },
+ set: function (value) {
+ this._bottom = value;
+ }
+ },
+
+ /**
+ * Indicates the the inset from the right.
+ * @type {Number}
+ * @memberof Insets.prototype
+ */
+ right: {
+ get: function () {
+ return this._right;
+ },
+ set: function (value) {
+ this._right = value;
+ }
+ }
+
+});
+
+export default Insets;
diff --git a/web/test/WebWorldWind/src/util/Level.js b/web/test/WebWorldWind/src/util/Level.js
new file mode 100644
index 00000000..33e7bb54
--- /dev/null
+++ b/web/test/WebWorldWind/src/util/Level.js
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports Level
+ */
+import Angle from '../geom/Angle';
+import ArgumentError from '../error/ArgumentError';
+import Logger from '../util/Logger';
+
+
+/**
+ * Constructs a Level within a [LevelSet]{@link LevelSet}. Applications typically do not interact with this
+ * class.
+ * @alias Level
+ * @constructor
+ * @classdesc Represents a level in a tile pyramid.
+ * @throws {ArgumentError} If either the specified tile delta or parent level set is null or undefined.
+ */
+function Level(levelNumber, tileDelta, parent) {
+ if (!tileDelta) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Level", "constructor",
+ "The specified tile delta is null or undefined"));
+ }
+
+ if (!parent) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Level", "constructor",
+ "The specified parent level set is null or undefined"));
+ }
+
+ /**
+ * The level's ordinal in its parent level set.
+ * @type {Number}
+ */
+ this.levelNumber = levelNumber;
+
+ /**
+ * The geographic size of tiles within this level.
+ * @type {Location}
+ */
+ this.tileDelta = tileDelta;
+
+ /**
+ * The level set that this level is a member of.
+ * @type {LevelSet}
+ */
+ this.parent = parent;
+
+ /**
+ * The size of pixels or elevation cells within this level, in radians per pixel or per cell.
+ * @type {Number}
+ */
+ this.texelSize = tileDelta.latitude * Angle.DEGREES_TO_RADIANS / parent.tileHeight;
+
+ /**
+ * The width in pixels or cells of the resource associated with tiles within this level.
+ * @type {Number}
+ */
+ this.tileWidth = parent.tileWidth;
+
+ /**
+ * The height in pixels or cells of the resource associated with tiles within this level.
+ * @type {Number}
+ */
+ this.tileHeight = parent.tileHeight;
+
+ /**
+ * The sector spanned by this level.
+ * @type {Sector}
+ */
+ this.sector = parent.sector;
+}
+
+/**
+ * Indicates whether this level is the lowest resolution level (level 0) within its parent's level set.
+ * @returns {Boolean} true If this tile is the lowest resolution in the parent level set,
+ * otherwise false.
+ */
+Level.prototype.isFirstLevel = function () {
+ return this.parent.firstLevel() == this;
+};
+
+/**
+ * Indicates whether this level is the highest resolution level within its parent's level set.
+ * @returns {Boolean} true If this tile is the highest resolution in the parent level set,
+ * otherwise false.
+ */
+Level.prototype.isLastLevel = function () {
+ return this.parent.lastLevel() == this;
+};
+
+/**
+ * Returns the level whose ordinal occurs immediately before this level's ordinal in the parent level set, or
+ * null if this is the fist level.
+ * @returns {Level} The previous level, or null if this is the first level.
+ */
+Level.prototype.previousLevel = function () {
+ return this.parent.level(this.levelNumber - 1);
+};
+
+/**
+ * Returns the level whose ordinal occurs immediately after this level's ordinal in the parent level set, or
+ * null if this is the last level.
+ * @returns {Level} The next level, or null if this is the last level.
+ */
+Level.prototype.nextLevel = function () {
+ return this.parent.level(this.levelNumber + 1);
+};
+
+/**
+ * Compare this level's ordinal to that of a specified level.
+ * @param {Level} that The level to compare this one to.
+ * @returns {Number} 0 if the two ordinals are equivalent. -1 if this level's ordinal is less than the specified
+ * level's ordinal. 1 if this level's ordinal is greater than the specified level's ordinal.
+ * @throws {ArgumentError} If the specified level is null or undefined.
+ */
+Level.prototype.compare = function (that) {
+ if (!that) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Level", "compare",
+ "The specified level is null or undefined"));
+ }
+
+ if (this.levelNumber < that.levelNumber)
+ return -1;
+
+ if (this.levelNumber > that.levelNumber)
+ return 1;
+
+ return 0;
+};
+
+export default Level;
diff --git a/web/test/WebWorldWind/src/util/LevelSet.js b/web/test/WebWorldWind/src/util/LevelSet.js
new file mode 100644
index 00000000..08d254d3
--- /dev/null
+++ b/web/test/WebWorldWind/src/util/LevelSet.js
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports LevelSet
+ */
+import ArgumentError from '../error/ArgumentError';
+import Level from '../util/Level';
+import Location from '../geom/Location';
+import Logger from '../util/Logger';
+
+
+/**
+ * Constructs a level set.
+ * @alias Level
+ * @constructor
+ * @classdesc Represents a multi-resolution, hierarchical collection of tiles. Applications typically do not
+ * interact with this class.
+ * @param {Sector} sector The sector spanned by this level set.
+ * @param {Location} levelZeroDelta The geographic size of tiles in the lowest resolution level of this level set.
+ * @param {Number} numLevels The number of levels in the level set.
+ * @param {Number} tileWidth The height in pixels of images associated with tiles in this level set, or the number of sample
+ * points in the longitudinal direction of elevation tiles associate with this level set.
+ * @param {Number} tileHeight The height in pixels of images associated with tiles in this level set, or the number of sample
+ * points in the latitudinal direction of elevation tiles associate with this level set.
+ * @throws {ArgumentError} If the specified sector or level-zero-delta is null or undefined, the level zero
+ * delta values are less than or equal to zero, or any of the number-of-levels, tile-width or tile-height
+ * arguments are less than 1.
+ */
+function LevelSet(sector, levelZeroDelta, numLevels, tileWidth, tileHeight) {
+ if (!sector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "LevelSet", "constructor", "missingSector"));
+ }
+
+ if (!levelZeroDelta) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "LevelSet", "constructor",
+ "The specified level zero delta is null or undefined"));
+ }
+
+ if (levelZeroDelta.latitude <= 0 || levelZeroDelta.longitude <= 0) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "LevelSet", "constructor",
+ "The specified level zero delta is less than or equal to zero."));
+ }
+
+ if (numLevels < 1) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "LevelSet", "constructor",
+ "The specified number of levels is less than one."));
+ }
+
+ if (tileWidth < 1 || tileHeight < 1) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "LevelSet", "constructor",
+ "The specified tile width or tile height is less than one."));
+ }
+
+ /**
+ * The sector spanned by this level set.
+ * @type {Sector}
+ * @readonly
+ */
+ this.sector = sector;
+
+ /**
+ * The geographic size of the lowest resolution (level 0) tiles in this level set.
+ * @type {Location}
+ * @readonly
+ */
+ this.levelZeroDelta = levelZeroDelta;
+
+ /**
+ * The number of levels in this level set.
+ * @type {Number}
+ * @readonly
+ */
+ this.numLevels = numLevels;
+
+ /**
+ * The width in pixels of images associated with tiles in this level set, or the number of sample points
+ * in the longitudinal direction of elevation tiles associated with this level set.
+ * @type {Number}
+ * @readonly
+ */
+ this.tileWidth = tileWidth;
+
+ /**
+ * The height in pixels of images associated with tiles in this level set, or the number of sample points
+ * in the latitudinal direction of elevation tiles associated with this level set.
+ * @type {Number}
+ * @readonly
+ */
+ this.tileHeight = tileHeight;
+
+ this.levels = [];
+
+ for (var i = 0; i < numLevels; i += 1) {
+ var n = Math.pow(2, i),
+ latDelta = levelZeroDelta.latitude / n,
+ lonDelta = levelZeroDelta.longitude / n,
+ tileDelta = new Location(latDelta, lonDelta),
+ level = new Level(i, tileDelta, this);
+
+ this.levels[i] = level;
+ }
+}
+
+/**
+ * Returns the number of levels that match the specified resolution. firstLevelResolution indicates the
+ * resolution of the first level's tiles, in degrees per pixel. This depends only on the LevelSet configuration,
+ * and is derived by evaluating {@code levelZeroDelta.latitude / tileHeight}. lastLevelResolution indicates the
+ * resolution of the data represented by the LevelSet.
+ *
+ * The returned level count is a fractional value. The dataset resolution is rarely an even multiple of the
+ * first level resolution, so the ideal last level is typically somewhere in between two levels. An integer
+ * level count can be computed depending on the desired behavior. If the last level should
+ * match or exceed the data resolution, take the ceiling of the returned value. Otherwise, if the last
+ * level should be no more than the data resolution, take the floor of the returned value.
+ *
+ * @param {Number} firstLevelResolution the known resolution of the first level in degrees per pixel
+ * @param {Number} lastLevelResolution the desired resolution of the last level in degrees per pixel
+ *
+ * @return {Number} the number of levels as a fractional level count
+ *
+ * @throws {ArgumentError} If either resolution is null, undefined, or zero
+ */
+LevelSet.numLevelsForResolution = function (firstLevelResolution, lastLevelResolution) {
+ if (!firstLevelResolution || !lastLevelResolution) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "LevelSet", "numLevelsForResolution", "missingResolution"));
+ }
+
+ var lastLevel = Math.log(firstLevelResolution / lastLevelResolution) / Math.log(2); // fractional level address
+
+ if (lastLevel < 0) {
+ lastLevel = 0; // ensure at least one level is used, resolution can be less than the first level resolution
+ }
+
+ return lastLevel + 1; // convert level number to level count
+};
+
+/**
+ * Returns the {@link Level} for a specified level number.
+ * @param {Number} levelNumber The number of the desired level.
+ * @returns {Level} The requested level, or null if the level does not exist.
+ */
+LevelSet.prototype.level = function (levelNumber) {
+ if (levelNumber < 0 || levelNumber >= this.levels.length) {
+ return null;
+ } else {
+ return this.levels[levelNumber];
+ }
+};
+
+/**
+ * Returns the level with a specified texel size.
+ * This function returns the first level if the specified texel size is greater than the first level's texel
+ * size, and returns the last level if the delta is less than the last level's texel size.
+ * @param {Number} texelSize The size of pixels or elevation cells in the level, in radians per pixel or cell.
+ */
+LevelSet.prototype.levelForTexelSize = function (texelSize) {
+ // TODO: Replace this loop with a computation.
+ var lastLevel = this.lastLevel();
+
+ if (lastLevel.texelSize >= texelSize) {
+ return lastLevel; // Can't do any better than the last level.
+ }
+
+ for (var index = 0, length = this.levels.length; index < length; index += 1) {
+ var level = this.levels[index];
+ if (level.texelSize <= texelSize) {
+ return level;
+ }
+ }
+
+ return lastLevel;
+};
+
+/**
+ * Returns the first (lowest resolution) level of this level set.
+ * @returns {Level} The first level of this level set.
+ */
+LevelSet.prototype.firstLevel = function () {
+ return this.levels[0];
+};
+
+/**
+ * Returns the last (highest resolution) level of this level set.
+ * @returns {Level} The last level of this level set.
+ */
+LevelSet.prototype.lastLevel = function () {
+ return this.levels[this.levels.length - 1];
+};
+
+export default LevelSet;
diff --git a/web/test/WebWorldWind/src/util/Logger.js b/web/test/WebWorldWind/src/util/Logger.js
new file mode 100644
index 00000000..03fa5b78
--- /dev/null
+++ b/web/test/WebWorldWind/src/util/Logger.js
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/**
+ * Logs selected message types to the console.
+ * @exports Logger
+ */
+
+var Logger = {
+ /**
+ * Log no messages.
+ * @constant
+ */
+ LEVEL_NONE: 0,
+ /**
+ * Log messages marked as severe.
+ * @constant
+ */
+ LEVEL_SEVERE: 1,
+ /**
+ * Log messages marked as warnings and messages marked as severe.
+ * @constant
+ */
+ LEVEL_WARNING: 2,
+ /**
+ * Log messages marked as information, messages marked as warnings and messages marked as severe.
+ * @constant
+ */
+ LEVEL_INFO: 3,
+
+ /**
+ * Set the logging level used by subsequent invocations of the logger.
+ * @param {Number} level The logging level, one of Logger.LEVEL_NONE, Logger.LEVEL_SEVERE, Logger.LEVEL_WARNING,
+ * or Logger.LEVEL_INFO.
+ */
+ setLoggingLevel: function (level) {
+ loggingLevel = level;
+ },
+
+ /**
+ * Indicates the current logging level.
+ * @returns {Number} The current logging level.
+ */
+ getLoggingLevel: function () {
+ return loggingLevel;
+ },
+
+ /**
+ * Logs a specified message at a specified level.
+ * @param {Number} level The logging level of the message. If the current logging level allows this message to be
+ * logged it is written to the console.
+ * @param {String} message The message to log. Nothing is logged if the message is null or undefined.
+ */
+ log: function (level, message) {
+ if (message && level > 0 && level <= loggingLevel) {
+ if (level === Logger.LEVEL_SEVERE) {
+ console.error(message);
+ } else if (level === Logger.LEVEL_WARNING) {
+ console.warn(message);
+ } else if (level === Logger.LEVEL_INFO) {
+ console.info(message);
+ } else {
+ console.log(message);
+ }
+ }
+ },
+
+ // Intentionally not documented.
+ makeMessage: function (className, functionName, message) {
+ var msg = this.messageTable[message] ? this.messageTable[message] : message;
+
+ return className + "." + functionName + ": " + msg;
+ },
+
+ // Intentionally not documented.
+ logMessage: function (level, className, functionName, message) {
+ var msg = this.makeMessage(className, functionName, message);
+ this.log(level, msg);
+
+ return msg;
+ },
+
+ // Intentionally not documented.
+ messageTable: { // KEEP THIS TABLE IN ALPHABETICAL ORDER
+ abstractInvocation: "The function called is abstract and must be overridden in a subclass.",
+ indexOutOfRange: "The specified index is out of range.",
+ invalidColumn: "The specified column is out of range.",
+ invalidHeight: "The specified height is zero or negative.",
+ invalidWidth: "The specified width is zero or negative.",
+ invalidRow: "The specified row is out of range.",
+ invalidSize: "The specified size is zero or negative.",
+ missingAltitudeMode: "The specified altitude mode is null or undefined.",
+ missingArrayBuffer: "The specified array buffer is null or undefined",
+ missingAttributeName: "The specified DBase attribute file name is null or undefined.",
+ missingArray: "The specified array is null, undefined or of insufficient length.",
+ missingBoundaries: "The specified boundaries array is null or undefined.",
+ missingBuffer: "The specified buffer descriptor is null or undefined.",
+ missingColor: "The specified color is null or undefined.",
+ missingConfig: "The specified config is null or undefined.",
+ missingDc: "The specified draw context is null or undefined.",
+ missingDomElement: "The specified DOM element is null or undefined.",
+ missingEntry: "The specified entry is null or undefined.",
+ missingFont: "The specified font is null or undefined.",
+ missingFrustum: "The specified frustum is null or undefined.",
+ missingFunction: "The specified function is null or undefined.",
+ missingGlContext: "The specified WebGL rendering context is null or undefined.",
+ missingGlobe: "The specified globe is null or undefined.",
+ missingId: "The specified id is null or undefined.",
+ missingImage: "The specified image is null or undefined.",
+ missingImageFormat: "The specified image format is null or undefined.",
+ missingIndices: "The specified indices array is null or undefined.",
+ missingKey: "The specified key is null or undefined.",
+ missingLevel: "The specified level is null or undefined.",
+ missingLine: "The specified line is null or undefined.",
+ missingList: "The specified list is null or undefined.",
+ missingListener: "The specified listener is null or undefined",
+ missingLocation: "The specified location is null or undefined.",
+ missingMatrix: "The specified matrix is null or undefined.",
+ missingOffset: "The specified offset is null or undefined.",
+ missingPath: "The specified path is null or undefined.",
+ missingPlacename: "The specified place name is null or undefined.",
+ missingPlane: "The specified plane is null or undefined.",
+ missingPoint: "The specified point is null or undefined.",
+ missingPoints: "The specified points array is null or undefined.",
+ missingPosition: "The specified position is null or undefined.",
+ missingPositions: "The specified positions array is null or undefined.",
+ missingProgram: "The specified program is null or undefined.",
+ missingProjection: "The specified projection is null or undefined.",
+ missingRectangle: "The specified rectangle is null or undefined.",
+ missingRenderable: "The specified renderable is null or undefined.",
+ missingResolution: "The specified resolution is null, undefined, or zero.",
+ missingResource: "The specified resource is null or undefined.",
+ missingResult: "The specified result variable is null or undefined.",
+ missingResults: "The specified results array is null or undefined.",
+ missingSector: "The specified sector is null or undefined.",
+ missingShapeType: "The specified shape type is null or undefined.",
+ missingSize: "The specified size is null or undefined.",
+ missingText: "The specified text is null or undefined.",
+ missingTexture: "The specified texture is null or undefined.",
+ missingTile: "The specified tile is null or undefined.",
+ missingType: "The specified type is null or undefined.",
+ missingUrl: "The specified URL is null or undefined",
+ missingVector: "The specified vector is null or undefined.",
+ missingVertex: "The specified vertex is null or undefined.",
+ missingViewport: "The specified viewport is null or undefined.",
+ missingWebCoverageService: "The specified WebCoverageService is null or undefined.",
+ missingWorldWindow: "The specified WorldWindow is null or undefined.",
+ notYetImplemented: "This function is not yet implemented",
+ unsupportedVersion: "The specified version is not supported.",
+ webglNotSupported: "The browser does not support WebGL, or WebGL is disabled."
+ }
+};
+
+var loggingLevel = 1; // log severe messages by default
+
+export default Logger;
diff --git a/web/test/WebWorldWind/src/util/Offset.js b/web/test/WebWorldWind/src/util/Offset.js
new file mode 100644
index 00000000..40b2047e
--- /dev/null
+++ b/web/test/WebWorldWind/src/util/Offset.js
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports Offset
+ */
+import Vec2 from '../geom/Vec2';
+
+
+/**
+ * Constructs an offset instance given specified units and offsets.
+ * @alias Offset
+ * @constructor
+ * @classdesc Specifies an offset relative to a rectangle. Used by [Placemark]{@link Placemark} and
+ * other shapes.
+ * @param {String} xUnits The type of units specified for the X dimension. May be one of the following:
+ *
+ * The tile's frame-dependent properties, include the extent (bounding volume). These properties are dependent
+ * on the tile's sector and the elevation values currently in memory, and change when those dependencies change.
+ * Therefore true to enable modulation, false to disable modulation.
+ */
+SurfaceTileRendererProgram.prototype.loadModulateColor = function (gl, enable) {
+ gl.uniform1i(this.modulateColorLocation, enable ? 1 : 0);
+};
+
+export default SurfaceTileRendererProgram;
diff --git a/web/test/WebWorldWind/src/shaders/glsl/basic_fragment.glsl b/web/test/WebWorldWind/src/shaders/glsl/basic_fragment.glsl
new file mode 100644
index 00000000..84856a26
--- /dev/null
+++ b/web/test/WebWorldWind/src/shaders/glsl/basic_fragment.glsl
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+precision mediump float;
+
+uniform vec4 color;
+
+void main() {
+ gl_FragColor = color;
+}
\ No newline at end of file
diff --git a/web/test/WebWorldWind/src/shaders/glsl/basic_texture_fragment.glsl b/web/test/WebWorldWind/src/shaders/glsl/basic_texture_fragment.glsl
new file mode 100644
index 00000000..af9ae1c5
--- /dev/null
+++ b/web/test/WebWorldWind/src/shaders/glsl/basic_texture_fragment.glsl
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+precision mediump float;
+
+uniform float opacity;
+uniform vec4 color;
+uniform bool enableTexture;
+uniform bool modulateColor;
+uniform sampler2D textureSampler;
+uniform bool applyLighting;
+
+varying vec2 texCoord;
+varying vec4 normal;
+
+void main() {
+ vec4 textureColor = texture2D(textureSampler, texCoord);
+ float ambient = 0.15;
+ vec4 lightDirection = vec4(0, 0, 1, 0);
+
+ if (enableTexture && !modulateColor)
+ gl_FragColor = textureColor * color * opacity;
+ else if (enableTexture && modulateColor)
+ gl_FragColor = color * floor(textureColor.a + 0.5);
+ else
+ gl_FragColor = color * opacity;
+ if (gl_FragColor.a == 0.0) {
+ discard;
+ }
+ if (applyLighting) {
+ vec4 n = normal * (gl_FrontFacing ? 1.0 : -1.0);
+ gl_FragColor.rgb *= clamp(ambient + dot(lightDirection, n), 0.0, 1.0);
+ }
+}
\ No newline at end of file
diff --git a/web/test/WebWorldWind/src/shaders/glsl/basic_texture_vertex.glsl b/web/test/WebWorldWind/src/shaders/glsl/basic_texture_vertex.glsl
new file mode 100644
index 00000000..dc964bc6
--- /dev/null
+++ b/web/test/WebWorldWind/src/shaders/glsl/basic_texture_vertex.glsl
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+attribute vec4 vertexPoint;
+attribute vec4 vertexTexCoord;
+attribute vec4 normalVector;
+
+uniform mat4 mvpMatrix;
+uniform mat4 mvInverseMatrix;
+uniform mat4 texCoordMatrix;
+uniform bool applyLighting;
+
+varying vec2 texCoord;
+varying vec4 normal;
+
+void main() {
+ gl_Position = mvpMatrix * vertexPoint;
+ texCoord = (texCoordMatrix * vertexTexCoord).st;
+ if (applyLighting) {
+ normal = mvInverseMatrix * normalVector;
+ }
+}
\ No newline at end of file
diff --git a/web/test/WebWorldWind/src/shaders/glsl/basic_vertex.glsl b/web/test/WebWorldWind/src/shaders/glsl/basic_vertex.glsl
new file mode 100644
index 00000000..2a741fac
--- /dev/null
+++ b/web/test/WebWorldWind/src/shaders/glsl/basic_vertex.glsl
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+attribute vec4 vertexPoint;
+
+uniform mat4 mvpMatrix;
+
+void main() {
+ gl_Position = mvpMatrix * vertexPoint;
+}
\ No newline at end of file
diff --git a/web/test/WebWorldWind/src/shaders/glsl/ground_fragment.glsl b/web/test/WebWorldWind/src/shaders/glsl/ground_fragment.glsl
new file mode 100644
index 00000000..d75a392a
--- /dev/null
+++ b/web/test/WebWorldWind/src/shaders/glsl/ground_fragment.glsl
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+precision mediump float;
+precision mediump int;
+
+const int FRAGMODE_GROUND_PRIMARY = 2;
+const int FRAGMODE_GROUND_SECONDARY = 3;
+const int FRAGMODE_GROUND_PRIMARY_TEX_BLEND = 4;
+
+uniform int fragMode;
+uniform sampler2D texSampler;
+
+varying vec3 primaryColor;
+varying vec3 secondaryColor;
+varying vec2 texCoord;
+
+void main (void) {
+ if (fragMode == FRAGMODE_GROUND_PRIMARY) {
+ gl_FragColor = vec4(primaryColor, 1.0);
+ } else if (fragMode == FRAGMODE_GROUND_SECONDARY) {
+ gl_FragColor = vec4(secondaryColor, 1.0);
+ } else if (fragMode == FRAGMODE_GROUND_PRIMARY_TEX_BLEND) {
+ vec4 texColor = texture2D(texSampler, texCoord);
+ gl_FragColor = vec4(primaryColor + texColor.rgb * (1.0 - secondaryColor), 1.0);
+ }
+}
\ No newline at end of file
diff --git a/web/test/WebWorldWind/src/shaders/glsl/ground_vertex.glsl b/web/test/WebWorldWind/src/shaders/glsl/ground_vertex.glsl
new file mode 100644
index 00000000..f30299b9
--- /dev/null
+++ b/web/test/WebWorldWind/src/shaders/glsl/ground_vertex.glsl
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+precision mediump int;
+
+const int FRAGMODE_GROUND_PRIMARY_TEX_BLEND = 4;
+const int SAMPLE_COUNT = 2;
+const float SAMPLES = 2.0;
+
+const float PI = 3.141592653589;
+const float Kr = 0.0025;
+const float Kr4PI = Kr * 4.0 * PI;
+const float Km = 0.0015;
+const float Km4PI = Km * 4.0 * PI;
+const float ESun = 15.0;
+const float KmESun = Km * ESun;
+const float KrESun = Kr * ESun;
+const vec3 invWavelength = vec3(5.60204474633241, 9.473284437923038, 19.643802610477206);
+const float rayleighScaleDepth = 0.25;
+
+uniform int fragMode;
+uniform mat4 mvpMatrix;
+uniform mat3 texCoordMatrix;
+uniform vec3 vertexOrigin;
+uniform vec3 eyePoint;
+uniform float eyeMagnitude; /* The eye point's magnitude */
+uniform float eyeMagnitude2; /* eyeMagnitude^2 */
+uniform vec3 lightDirection; /* The direction vector to the light source */
+uniform float atmosphereRadius; /* The outer (atmosphere) radius */
+uniform float atmosphereRadius2; /* atmosphereRadius^2 */
+uniform float globeRadius; /* The inner (planetary) radius */
+uniform float scale; /* 1 / (atmosphereRadius - globeRadius) */
+uniform float scaleDepth; /* The scale depth (i.e. the altitude at which
+ the atmosphere's average density is found) */
+uniform float scaleOverScaleDepth; /* fScale / fScaleDepth */
+
+attribute vec4 vertexPoint;
+attribute vec2 vertexTexCoord;
+
+varying vec3 primaryColor;
+varying vec3 secondaryColor;
+varying vec2 texCoord;
+
+float scaleFunc(float cos) {
+ float x = 1.0 - cos;
+ return scaleDepth * exp(-0.00287 + x*(0.459 + x*(3.83 + x*(-6.80 + x*5.25))));
+}
+
+void sampleGround() {
+ /* Get the ray from the camera to the vertex and its length (which is the far point of the ray passing through the
+ atmosphere) */
+ vec3 point = vertexPoint.xyz + vertexOrigin;
+ vec3 ray = point - eyePoint;
+ float far = length(ray);
+ ray /= far;
+
+ vec3 start;
+ if (eyeMagnitude < atmosphereRadius) {
+ start = eyePoint;
+ } else {
+ /* Calculate the closest intersection of the ray with the outer atmosphere (which is the near point of the ray
+ passing through the atmosphere) */
+ float B = 2.0 * dot(eyePoint, ray);
+ float C = eyeMagnitude2 - atmosphereRadius2;
+ float det = max(0.0, B*B - 4.0 * C);
+ float near = 0.5 * (-B - sqrt(det));
+
+ /* Calculate the ray's starting point, then calculate its scattering offset */
+ start = eyePoint + ray * near;
+ far -= near;
+ }
+ float depth = exp((globeRadius - atmosphereRadius) / scaleDepth);
+ float eyeAngle = dot(-ray, point) / length(point);
+ float lightAngle = dot(lightDirection, point) / length(point);
+ float eyeScale = scaleFunc(eyeAngle);
+ float lightScale = scaleFunc(lightAngle);
+ float eyeOffset = depth*eyeScale;
+ float temp = (lightScale + eyeScale);
+
+ /* Initialize the scattering loop variables */
+ float sampleLength = far / SAMPLES;
+ float scaledLength = sampleLength * scale;
+ vec3 sampleRay = ray * sampleLength;
+ vec3 samplePoint = start + sampleRay * 0.5;
+
+ /* Now loop through the sample rays */
+ vec3 frontColor = vec3(0.0, 0.0, 0.0);
+ vec3 attenuate = vec3(0.0, 0.0, 0.0);
+ for(int i=0; i
+ *
+ * @type {String}
+ * @default WorldWind.ABSOLUTE
+ * @memberof AbstractShape.prototype
+ */
+ altitudeMode: {
+ get: function () {
+ return this._altitudeMode;
+ },
+ set: function (altitudeMode) {
+ if (!altitudeMode) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "AbstractShape",
+ "altitudeMode", "missingAltitudeMode"));
+ }
+
+ this._altitudeMode = altitudeMode;
+ this.reset();
+ }
+ }
+});
+
+/**
+ * Clears this shape's data cache. Should be called by subclasses when state changes invalidate
+ * cached data.
+ * @protected
+ */
+AbstractShape.prototype.reset = function () {
+ this.shapeDataCache.clear(false);
+ this.surfaceShape = null;
+};
+
+AbstractShape.prototype.updateSurfaceShape = function () {
+ // Synchronize this AbstractShape's properties with its SurfaceShape's properties. Note that the attributes
+ // and the highlightAttributes are synchronized separately.
+ this.surfaceShape.displayName = this.displayName;
+ this.surfaceShape.highlighted = this.highlighted;
+ this.surfaceShape.enabled = this.enabled;
+ this.surfaceShape.pathType = this.pathType;
+ this.surfaceShape.pickDelegate = this.pickDelegate ? this.pickDelegate : this;
+};
+
+AbstractShape.prototype.createSurfaceShape = function () {
+ return null;
+};
+
+AbstractShape.prototype.render = function (dc) {
+ if (!this.enabled) {
+ return;
+ }
+
+ if (!dc.accumulateOrderedRenderables) {
+ return;
+ }
+
+ if (dc.globe.is2D() && this.useSurfaceShapeFor2D) {
+ if (!this.surfaceShape) {
+ this.surfaceShape = this.createSurfaceShape();
+ if (this.surfaceShape) {
+ this.surfaceShape.attributes = this._attributes;
+ this.surfaceShape.highlightAttributes = this._highlightAttributes;
+ }
+ }
+
+ if (this.surfaceShape) {
+ this.updateSurfaceShape();
+ this.surfaceShape.render(dc);
+ return;
+ }
+ }
+
+ if (!dc.terrain && this.altitudeMode != WorldWind.ABSOLUTE) {
+ return;
+ }
+
+ this.establishCurrentData(dc);
+
+ if (dc.globe.projectionLimits && !this.isWithinProjectionLimits(dc)) {
+ return;
+ }
+
+ // Use the last computed extent to see if this shape is out of view.
+ if (this.currentData.extent && !this.intersectsFrustum(dc)) {
+ return;
+ }
+
+ this.determineActiveAttributes(dc);
+ if (!this.activeAttributes) {
+ return;
+ }
+
+ var orderedRenderable = this.makeOrderedRenderable(dc);
+ if (orderedRenderable) {
+
+ // Use the updated extent to see if this shape is out of view.
+ if (!this.intersectsFrustum(dc)) {
+ return;
+ }
+
+ if (dc.isSmall(this.currentData.extent, 1)) {
+ return;
+ }
+
+ orderedRenderable.layer = dc.currentLayer;
+ dc.addOrderedRenderable(orderedRenderable, this.currentData.eyeDistance);
+ }
+};
+
+/**
+ * Draws this shape during ordered rendering. Implements the {@link OrderedRenderable} interface.
+ * This method is called by the WorldWindow and is not intended to be called by applications.
+ * @param {DrawContext} dc The current draw context.
+ */
+AbstractShape.prototype.renderOrdered = function (dc) {
+ this.currentData = this.shapeDataCache.entryForKey(dc.globeStateKey);
+
+ this.beginDrawing(dc);
+ try {
+ this.doRenderOrdered(dc);
+ } finally {
+ this.endDrawing(dc);
+ }
+};
+
+// Internal. Intentionally not documented.
+AbstractShape.prototype.makeOrderedRenderable = function (dc) {
+ var or = this.doMakeOrderedRenderable(dc);
+ this.currentData.verticalExaggeration = dc.verticalExaggeration;
+
+ return or;
+};
+
+/**
+ * Called during rendering. Subclasses must override this method with one that creates and enques an
+ * ordered renderable for this shape if this shape is to be displayed. Applications do not call this method.
+ * @param {DrawContext} dc The current draw context.
+ * @protected
+ */
+AbstractShape.prototype.doMakeOrderedRenderable = function (dc) {
+ throw new UnsupportedOperationError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "AbstractShape", "makeOrderedRenderable", "abstractInvocation"));
+};
+
+/**
+ * Called during ordered rendering. Subclasses must override this method to render the shape using the current
+ * shape data.
+ * @param {DrawContext} dc The current draw context.
+ * @protected
+ */
+AbstractShape.prototype.doRenderOrdered = function (dc) {
+ throw new UnsupportedOperationError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "AbstractShape", "doRenderOrdered", "abstractInvocation"));
+};
+
+/**
+ * Called during ordered rendering. Subclasses may override this method in order to perform operations prior
+ * to drawing the shape. Applications do not call this method.
+ * @param {DrawContext} dc The current draw context.
+ * @protected
+ */
+AbstractShape.prototype.beginDrawing = function (dc) {
+};
+
+/**
+ * Called during ordered rendering. Subclasses may override this method in order to perform operations after
+ * the shape is drawn. Applications do not call this method.
+ * @param {DrawContext} dc The current draw context.
+ * @protected
+ */
+AbstractShape.prototype.endDrawing = function (dc) {
+};
+
+// Internal. Intentionally not documented.
+AbstractShape.prototype.intersectsFrustum = function (dc) {
+ if (this.currentData && this.currentData.extent) {
+ if (dc.pickingMode) {
+ return this.currentData.extent.intersectsFrustum(dc.pickFrustum);
+ } else {
+ return this.currentData.extent.intersectsFrustum(dc.frustumInModelCoordinates);
+ }
+ } else {
+ return true;
+ }
+};
+
+// Internal. Intentionally not documented.
+AbstractShape.prototype.establishCurrentData = function (dc) {
+ this.currentData = this.shapeDataCache.entryForKey(dc.globeStateKey);
+ if (!this.currentData) {
+ this.currentData = this.createShapeDataObject();
+ this.resetExpiration(this.currentData);
+ this.shapeDataCache.putEntry(dc.globeStateKey, this.currentData, 1);
+ }
+
+ this.currentData.isExpired = !this.isShapeDataCurrent(dc, this.currentData);
+};
+
+/**
+ * Creates a new shape data object for the current globe state. Subclasses may override this method to
+ * modify the shape data object that this method creates, but must also call this method on this base class.
+ * Applications do not call this method.
+ * @returns {Object} The shape data object.
+ * @protected
+ */
+AbstractShape.prototype.createShapeDataObject = function () {
+ return {
+ transformationMatrix: Matrix.fromIdentity(),
+ referencePoint: new Vec3(0, 0, 0)
+ };
+};
+
+// Intentionally not documented.
+AbstractShape.prototype.resetExpiration = function (shapeData) {
+ // The random addition in the line below prevents all shapes from regenerating during the same frame.
+ shapeData.expiryTime = Date.now() + this.expirationInterval + 1e3 * Math.random();
+};
+
+/**
+ * Indicates whether a specified shape data object is current. Subclasses may override this method to add
+ * criteria indicating whether the shape data object is current, but must also call this method on this base
+ * class. Applications do not call this method.
+ * @param {DrawContext} dc The current draw context.
+ * @param {Object} shapeData The object to validate.
+ * @returns {Boolean} true if the object is current, otherwise false.
+ * @protected
+ */
+AbstractShape.prototype.isShapeDataCurrent = function (dc, shapeData) {
+ return shapeData.verticalExaggeration === dc.verticalExaggeration
+ && shapeData.expiryTime > Date.now();
+};
+
+// Internal. Intentionally not documented.
+AbstractShape.prototype.determineActiveAttributes = function (dc) {
+ if (this.highlighted && this._highlightAttributes) {
+ this.activeAttributes = this.highlightAttributes;
+ } else {
+ this.activeAttributes = this._attributes;
+ }
+};
+
+/**
+ * Indicates whether this shape is within the current globe's projection limits. Subclasses may implement
+ * this method to perform the test. The default implementation returns true. Applications do not call this
+ * method.
+ * @param {DrawContext} dc The current draw context.
+ * @returns {Boolean} true if this shape is is within or intersects the current globe's projection limits,
+ * otherwise false.
+ * @protected
+ */
+AbstractShape.prototype.isWithinProjectionLimits = function (dc) {
+ return true;
+};
+
+/**
+ * Apply the current navigator's model-view-projection matrix.
+ * @param {DrawContext} dc The current draw context.
+ * @protected
+ */
+AbstractShape.prototype.applyMvpMatrix = function (dc) {
+ this.scratchMatrix.copy(dc.modelviewProjection);
+ this.scratchMatrix.multiplyMatrix(this.currentData.transformationMatrix);
+ dc.currentProgram.loadModelviewProjection(dc.currentGlContext, this.scratchMatrix);
+};
+
+/**
+ * Apply the current navigator's model-view-projection matrix with an offset to make this shape's outline
+ * stand out.
+ * @param {DrawContext} dc The current draw context.
+ * @protected
+ */
+AbstractShape.prototype.applyMvpMatrixForOutline = function (dc) {
+ // Causes the outline to stand out from the interior.
+ this.scratchMatrix.copy(dc.projection);
+ this.scratchMatrix.offsetProjectionDepth(-0.001);
+ this.scratchMatrix.multiplyMatrix(dc.modelview);
+ this.scratchMatrix.multiplyMatrix(this.currentData.transformationMatrix);
+ dc.currentProgram.loadModelviewProjection(dc.currentGlContext, this.scratchMatrix);
+};
+
+export default AbstractShape;
diff --git a/web/test/WebWorldWind/src/shapes/Annotation.js b/web/test/WebWorldWind/src/shapes/Annotation.js
new file mode 100644
index 00000000..6b9a6b6d
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/Annotation.js
@@ -0,0 +1,515 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import AnnotationAttributes from '../shapes/AnnotationAttributes';
+import ArgumentError from '../error/ArgumentError';
+import BasicTextureProgram from '../shaders/BasicTextureProgram';
+import Color from '../util/Color';
+import Font from '../util/Font';
+import Insets from '../util/Insets';
+import Logger from '../util/Logger';
+import Matrix from '../geom/Matrix';
+import Offset from '../util/Offset';
+import PickedObject from '../pick/PickedObject';
+import Renderable from '../render/Renderable';
+import TextAttributes from '../shapes/TextAttributes';
+import Vec2 from '../geom/Vec2';
+import Vec3 from '../geom/Vec3';
+import WWMath from '../util/WWMath';
+
+
+/**
+ * Constructs an annotation.
+ * @alias Annotation
+ * @constructor
+ * @augments Renderable
+ * @classdesc Represents an Annotation shape. An annotation displays a callout, a text and a leader pointing
+ * the annotation's geographic position to the ground.
+ * @param {Position} position The annotations's geographic position.
+ * @param {AnnotationAttributes} attributes The attributes to associate with this annotation.
+ * @throws {ArgumentError} If the specified position is null or undefined.
+ */
+function Annotation(position, attributes) {
+
+ if (!position) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Annotation", "constructor", "missingPosition"));
+ }
+
+ Renderable.call(this);
+
+ /**
+ * This annotation's geographic position.
+ * @type {Position}
+ */
+ this.position = position;
+
+ /**
+ * The annotation's attributes.
+ * @type {AnnotationAttributes}
+ * @default see [AnnotationAttributes]{@link AnnotationAttributes}
+ */
+ this.attributes = attributes ? attributes : new AnnotationAttributes(null);
+
+ /**
+ * This annotation's altitude mode. May be one of
+ *
+ *
+ * @default WorldWind.ABSOLUTE
+ */
+ this.altitudeMode = WorldWind.ABSOLUTE;
+
+ // Internal use only. Intentionally not documented.
+ this.layer = null;
+
+ // Internal use only. Intentionally not documented.
+ this.lastStateKey = null;
+
+ // Internal use only. Intentionally not documented.
+ this.calloutTransform = Matrix.fromIdentity();
+
+ // Internal use only. Intentionally not documented.
+ this.calloutOffset = new WorldWind.Offset(
+ WorldWind.OFFSET_FRACTION, 0.5,
+ WorldWind.OFFSET_FRACTION, 0);
+
+ // Internal use only. Intentionally not documented.
+ this.label = "";
+
+ // Internal use only. Intentionally not documented.
+ this.labelTexture = null;
+
+ // Internal use only. Intentionally not documented.
+ this.labelTransform = Matrix.fromIdentity();
+
+ // Internal use only. Intentionally not documented.
+ this.placePoint = new Vec3(0, 0, 0);
+
+ // Internal use only. Intentionally not documented.
+ this.depthOffset = -2.05;
+
+ // Internal use only. Intentionally not documented.
+ this.calloutPoints = null;
+}
+
+Annotation.matrix = Matrix.fromIdentity();
+Annotation.screenPoint = new Vec3(0, 0, 0);
+Annotation.scratchPoint = new Vec3(0, 0, 0);
+
+Annotation.prototype = Object.create(Renderable.prototype);
+
+Object.defineProperties(Annotation.prototype, {
+
+ /**
+ * The text for this annotation.
+ * @type {String}
+ * @memberof Annotation.prototype
+ */
+ text: {
+ get: function () {
+ return this.label;
+ },
+ set: function (value) {
+ this.label = value;
+ this.lastStateKey = null;
+ }
+ }
+});
+
+/**
+ * Draws this shape as an ordered renderable. Applications do not call this function. It is called by
+ * [WorldWindow]{@link WorldWindow} during rendering.
+ * @param {DrawContext} dc The current draw context.
+ */
+Annotation.prototype.renderOrdered = function (dc) {
+
+ this.drawOrderedAnnotation(dc);
+
+ if (dc.pickingMode) {
+
+ var po = new PickedObject(this.pickColor.clone(), this,
+ this.position, this.layer, false);
+
+ if (dc.pickPoint) {
+ if (this.labelBounds.containsPoint(
+ dc.convertPointToViewport(dc.pickPoint, Annotation.scratchPoint))) {
+ po.labelPicked = true;
+ }
+ }
+
+ dc.resolvePick(po);
+ }
+};
+
+/**
+ * Creates a new annotation that is a copy of this annotation.
+ * @returns {Annotation} The new annotation.
+ */
+Annotation.prototype.clone = function () {
+ var clone = new Annotation(this.position);
+
+ clone.copy(this);
+ clone.pickDelegate = this.pickDelegate ? this.pickDelegate : this;
+
+ return clone;
+};
+
+/**
+ * Copies the contents of a specified annotation to this annotation.
+ * @param {Annotation} that The Annotation to copy.
+ */
+Annotation.prototype.copy = function (that) {
+ this.position = that.position;
+ this.enabled = that.enabled;
+ this.attributes = that.attributes;
+ this.label = that.label;
+ this.altitudeMode = that.altitudeMode;
+ this.pickDelegate = that.pickDelegate;
+ this.depthOffset = that.depthOffset;
+
+ return this;
+};
+
+/**
+ * Renders this annotation. This method is typically not called by applications but is called by
+ * {@link RenderableLayer} during rendering. For this shape this method creates and
+ * enques an ordered renderable with the draw context and does not actually draw the annotation.
+ * @param {DrawContext} dc The current draw context.
+ */
+Annotation.prototype.render = function (dc) {
+
+ if (!this.enabled) {
+ return;
+ }
+
+ if (!dc.accumulateOrderedRenderables) {
+ return;
+ }
+
+ if (dc.globe.projectionLimits
+ && !dc.globe.projectionLimits.containsLocation(this.position.latitude, this.position.longitude)) {
+ return;
+ }
+
+ var orderedAnnotation;
+ if (this.lastFrameTime !== dc.timestamp) {
+ orderedAnnotation = this.makeOrderedRenderable(dc);
+ } else {
+ var annotationCopy = this.clone();
+ orderedAnnotation = annotationCopy.makeOrderedRenderable(dc);
+ }
+
+ if (!orderedAnnotation) {
+ return;
+ }
+
+ orderedAnnotation.layer = dc.currentLayer;
+
+ this.lastFrameTime = dc.timestamp;
+ dc.addOrderedRenderable(orderedAnnotation);
+};
+
+// Internal. Intentionally not documented.
+Annotation.prototype.drawOrderedAnnotation = function (dc) {
+ this.beginDrawing(dc);
+
+ try {
+ this.doDrawOrderedAnnotation(dc);
+ } finally {
+ this.endDrawing(dc);
+ }
+};
+
+/* Intentionally not documented
+ * Creates an ordered renderable for this shape.
+ * @protected
+ * @param {DrawContext} dc The current draw context.
+ * @returns {OrderedRenderable} The ordered renderable. May be null, in which case an ordered renderable
+ * cannot be created or should not be created at the time this method is called.
+ */
+Annotation.prototype.makeOrderedRenderable = function (dc) {
+
+ var w, h, s, iLeft, iRight, iTop, iBottom,
+ offset, leaderGapHeight;
+
+ // Wraps the text based and the width and height that were set for the
+ // annotation
+ this.label = dc.textRenderer.wrap(
+ this.label,
+ this.attributes.width, this.attributes.height);
+
+ // Compute the annotation's model point.
+ dc.surfacePointForMode(this.position.latitude, this.position.longitude, this.position.altitude,
+ this.altitudeMode, this.placePoint);
+
+ this.eyeDistance = dc.eyePoint.distanceTo(this.placePoint);
+
+ // Compute the annotation's screen point in the OpenGL coordinate system of the WorldWindow
+ // by projecting its model coordinate point onto the viewport. Apply a depth offset in order
+ // to cause the annotation to appear above nearby terrain.
+ if (!dc.projectWithDepth(this.placePoint, this.depthOffset, Annotation.screenPoint)) {
+ return null;
+ }
+
+ this.labelTexture = dc.createTextTexture(this.label, this.attributes.textAttributes);
+
+ w = this.labelTexture.imageWidth;
+ h = this.labelTexture.imageHeight;
+ s = this.attributes.scale;
+ iLeft = this.attributes.insets.left;
+ iRight = this.attributes.insets.right;
+ iTop = this.attributes.insets.top;
+ iBottom = this.attributes.insets.bottom;
+ leaderGapHeight = this.attributes.leaderGapHeight;
+
+ offset = this.calloutOffset.offsetForSize((w + iLeft + iRight) * s, (h + iTop + iBottom) * s);
+
+ this.calloutTransform.setTranslation(
+ Annotation.screenPoint[0] - offset[0],
+ Annotation.screenPoint[1] + leaderGapHeight,
+ Annotation.screenPoint[2]);
+
+ this.labelTransform.setTranslation(
+ Annotation.screenPoint[0] - offset[0] + iLeft * s,
+ Annotation.screenPoint[1] + leaderGapHeight + iBottom * s,
+ Annotation.screenPoint[2]);
+
+ this.labelTransform.setScale(w * s, h * s, 1);
+
+ this.labelBounds = WWMath.boundingRectForUnitQuad(this.labelTransform);
+
+ // Compute dimensions of the callout taking in consideration the insets
+ var width = (w + iLeft + iRight) * s;
+ var height = (h + iTop + iBottom) * s;
+
+ var leaderOffsetX = width / 2;
+
+ var leaderOffsetY = -leaderGapHeight;
+
+ if (!this.attributes.drawLeader) {
+ leaderOffsetY = 0;
+ }
+
+ if (this.attributes.stateKey !== this.lastStateKey) {
+ this.calloutPoints = this.createCallout(
+ width, height,
+ leaderOffsetX, leaderOffsetY,
+ this.attributes.leaderGapWidth, this.attributes.cornerRadius);
+ }
+
+ return this;
+};
+
+// Internal. Intentionally not documented.
+Annotation.prototype.beginDrawing = function (dc) {
+ var gl = dc.currentGlContext,
+ program;
+
+ dc.findAndBindProgram(BasicTextureProgram);
+
+ program = dc.currentProgram;
+
+ gl.enableVertexAttribArray(program.vertexPointLocation);
+ gl.enableVertexAttribArray(program.vertexTexCoordLocation);
+
+ program.loadModulateColor(gl, dc.pickingMode);
+};
+
+// Internal. Intentionally not documented.
+Annotation.prototype.endDrawing = function (dc) {
+ var gl = dc.currentGlContext,
+ program = dc.currentProgram;
+
+ // Clear the vertex attribute state.
+ gl.disableVertexAttribArray(program.vertexPointLocation);
+ gl.disableVertexAttribArray(program.vertexTexCoordLocation);
+
+ // Clear GL bindings.
+ dc.bindProgram(null);
+};
+
+// Internal. Intentionally not documented.
+Annotation.prototype.drawCorner = function (x0, y0, cornerRadius, start, end, steps, buffer, startIdx) {
+ if (cornerRadius < 1) {
+ return startIdx;
+ }
+
+ var step = (end - start) / (steps - 1);
+ for (var i = 1; i < steps - 1; i++) {
+ var a = start + step * i;
+ var x = x0 + Math.cos(a) * cornerRadius;
+ var y = y0 + Math.sin(a) * cornerRadius;
+ buffer[startIdx++] = x;
+ buffer[startIdx++] = y;
+ }
+
+ return startIdx;
+};
+
+// Internal. Intentionally not documented.
+Annotation.prototype.createCallout = function (width, height, leaderOffsetX, leaderOffsetY, leaderGapWidth,
+ cornerRadius) {
+
+ var cornerSteps = 16;
+
+ var numVertices = 2 * (12 + (cornerRadius < 1 ? 0 : 4 * (cornerSteps - 2)));
+
+ var buffer = new Float32Array(numVertices);
+
+ var idx = 0;
+
+ //Bottom right
+ buffer[idx++] = width / 2 + leaderGapWidth / 2;
+ buffer[idx++] = 0;
+ buffer[idx++] = width - cornerRadius;
+ buffer[idx++] = 0;
+ idx = this.drawCorner(width - cornerRadius, cornerRadius, cornerRadius, -Math.PI / 2, 0,
+ cornerSteps, buffer, idx);
+
+ //Right
+ buffer[idx++] = width;
+ buffer[idx++] = cornerRadius;
+ buffer[idx++] = width;
+ buffer[idx++] = height - cornerRadius;
+ idx = this.drawCorner(width - cornerRadius, height - cornerRadius, cornerRadius, 0, Math.PI / 2,
+ cornerSteps, buffer, idx);
+
+ //Top
+ buffer[idx++] = width - cornerRadius;
+ buffer[idx++] = height;
+ buffer[idx++] = cornerRadius;
+ buffer[idx++] = height;
+ idx = this.drawCorner(cornerRadius, height - cornerRadius, cornerRadius, Math.PI / 2, Math.PI,
+ cornerSteps, buffer, idx);
+
+ //Left
+ buffer[idx++] = 0;
+ buffer[idx++] = height - cornerRadius;
+ buffer[idx++] = 0;
+ buffer[idx++] = cornerRadius;
+ idx = this.drawCorner(cornerRadius, cornerRadius, cornerRadius, Math.PI, Math.PI * 1.5,
+ cornerSteps, buffer, idx);
+
+ //Bottom left
+ buffer[idx++] = cornerRadius;
+ buffer[idx++] = 0;
+ buffer[idx++] = width / 2 - leaderGapWidth / 2;
+ buffer[idx++] = 0;
+
+ //Draw leader
+ buffer[idx++] = leaderOffsetX;
+ buffer[idx++] = leaderOffsetY;
+
+ buffer[idx++] = width / 2 + leaderGapWidth / 2;
+ buffer[idx] = 0;
+
+ return buffer;
+};
+
+// Internal. Intentionally not documented.
+Annotation.prototype.doDrawOrderedAnnotation = function (dc) {
+
+ var gl = dc.currentGlContext,
+ program = dc.currentProgram,
+ textureBound;
+
+ var refreshBuffers = false;
+
+ if (dc.pickingMode) {
+ this.pickColor = dc.uniquePickColor();
+ }
+
+ program.loadOpacity(gl, dc.pickingMode ? 1 : this.attributes.opacity * this.layer.opacity);
+
+ // Attributes have changed. We need to track this because the callout vbo data may
+ // have changed if scaled or text wrapping changes callout dimensions
+ var calloutAttributesChanged = this.attributes.stateKey !== this.lastStateKey;
+
+ // Create new cache key if callout drawing points have changed
+ if (!this.calloutCacheKey || calloutAttributesChanged) {
+ this.calloutCacheKey = dc.gpuResourceCache.generateCacheKey();
+ }
+
+ var calloutVboId = dc.gpuResourceCache.resourceForKey(this.calloutCacheKey);
+
+ if (!calloutVboId) {
+ calloutVboId = gl.createBuffer();
+ dc.gpuResourceCache.putResource(this.calloutCacheKey, calloutVboId,
+ this.calloutPoints.length * 4);
+
+ refreshBuffers = true;
+ }
+
+ // Remove the last generated vbo data if attributes changed
+ if (calloutAttributesChanged && this.calloutCacheKey) {
+ dc.gpuResourceCache.removeResource(this.calloutCacheKey);
+ }
+
+ // Store current statekey because we are no longer using it
+ // in this iteration
+ this.lastStateKey = this.attributes.stateKey;
+
+ // Compute and specify the MVP matrix.
+ Annotation.matrix.copy(dc.screenProjection);
+ Annotation.matrix.multiplyMatrix(this.calloutTransform);
+ program.loadModelviewProjection(gl, Annotation.matrix);
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, calloutVboId);
+
+ if (refreshBuffers) {
+ gl.bufferData(gl.ARRAY_BUFFER,
+ this.calloutPoints, gl.STATIC_DRAW);
+
+ dc.frameStatistics.incrementVboLoadCount(1);
+ }
+
+ program.loadColor(gl, dc.pickingMode ? this.pickColor : this.attributes.backgroundColor);
+ program.loadTextureEnabled(gl, false);
+
+ gl.vertexAttribPointer(program.vertexPointLocation, 2, gl.FLOAT, false, 0, 0);
+ gl.vertexAttribPointer(program.vertexTexCoordLocation, 2, gl.FLOAT, false, 0, 0);
+
+ gl.drawArrays(gl.TRIANGLE_FAN, 0, this.calloutPoints.length / 2);
+
+ // Draw text
+ Annotation.matrix.copy(dc.screenProjection);
+ Annotation.matrix.multiplyMatrix(this.labelTransform);
+ program.loadModelviewProjection(gl, Annotation.matrix);
+
+ Annotation.matrix.setToIdentity();
+ Annotation.matrix.multiplyByTextureTransform(this.labelTexture);
+ program.loadTextureMatrix(gl, Annotation.matrix);
+
+ program.loadColor(gl, dc.pickingMode ? this.pickColor : Color.WHITE);
+ textureBound = this.labelTexture.bind(dc);
+ program.loadTextureEnabled(gl, textureBound);
+
+ // Configure GL to use the draw context's unit quad VBOs for both model coordinates and texture coordinates.
+ // Most browsers can share the same buffer for vertex and texture coordinates, but Internet Explorer requires
+ // that they be in separate buffers, so the code below uses the 3D buffer for vertex coords and the 2D
+ // buffer for texture coords.
+ gl.bindBuffer(gl.ARRAY_BUFFER, dc.unitQuadBuffer3());
+ gl.vertexAttribPointer(program.vertexPointLocation, 3, gl.FLOAT, false, 0, 0);
+ gl.bindBuffer(gl.ARRAY_BUFFER, dc.unitQuadBuffer());
+ gl.vertexAttribPointer(program.vertexTexCoordLocation, 2, gl.FLOAT, false, 0, 0);
+
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+};
+
+export default Annotation;
diff --git a/web/test/WebWorldWind/src/shapes/AnnotationAttributes.js b/web/test/WebWorldWind/src/shapes/AnnotationAttributes.js
new file mode 100644
index 00000000..87488644
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/AnnotationAttributes.js
@@ -0,0 +1,285 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import Color from '../util/Color';
+import Font from '../util/Font';
+import Insets from '../util/Insets';
+import TextAttributes from '../shapes/TextAttributes';
+
+
+/**
+ * Constructs an annotation attributes bundle.
+ * @alias AnnotationAttributes
+ * @constructor
+ * @classdesc Holds attributes applied to {@link Annotation} shapes.
+ * @param {AnnotationAttributes} attributes Attributes to initialize this attributes instance to. May be null,
+ * in which case the new instance contains default attributes.
+ */
+function AnnotationAttributes(attributes) {
+
+ // These are all documented with their property accessors below.
+ this._cornerRadius = attributes ? attributes._cornerRadius : 0;
+ this._insets = attributes ? attributes._insets : new Insets(0, 0, 0, 0);
+ this._backgroundColor = attributes ? attributes._backgroundColor.clone() : Color.WHITE.clone();
+ this._leaderGapWidth = attributes ? attributes._leaderGapWidth : 40;
+ this._leaderGapHeight = attributes ? attributes._leaderGapHeight : 30;
+ this._opacity = attributes ? attributes._opacity : 1;
+ this._scale = attributes ? attributes._scale : 1;
+ this._drawLeader = attributes ? attributes._drawLeader : true;
+ this._width = attributes ? attributes._width : 200;
+ this._height = attributes ? attributes._height : 100;
+ this._textAttributes = attributes ? attributes._textAttributes : this.createDefaultTextAttributes();
+
+ /**
+ * Indicates whether this object's state key is invalid. Subclasses must set this value to true when their
+ * attributes change. The state key will be automatically computed the next time it's requested. This flag
+ * will be set to false when that occurs.
+ * @type {Boolean}
+ * @protected
+ */
+ this.stateKeyInvalid = true;
+}
+
+/**
+ * Computes the state key for this attributes object. Subclasses that define additional attributes must
+ * override this method, call it from that method, and append the state of their attributes to its
+ * return value.
+ * @returns {String} The state key for this object.
+ * @protected
+ */
+AnnotationAttributes.prototype.computeStateKey = function () {
+ return "wi " + this._width
+ + " he " + this._height
+ + " cr " + this._cornerRadius
+ + " in " + this._insets.toString()
+ + " bg " + this.backgroundColor.toHexString(true)
+ + " dl " + this.drawLeader
+ + " lgw " + this.leaderGapWidth
+ + " lgh " + this.leaderGapHeight
+ + " op " + this.opacity
+ + " ta " + this._textAttributes.stateKey
+ + " sc " + this.scale;
+};
+
+// Internal use only. Intentionally not documented.
+AnnotationAttributes.prototype.createDefaultTextAttributes = function () {
+ var attributes = new TextAttributes(null);
+ attributes.enableOutline = false; // Annotations display text without an outline by default
+ return attributes;
+};
+
+Object.defineProperties(AnnotationAttributes.prototype, {
+
+ /**
+ * Indicates the width of the callout.
+ * @type {Number}
+ * @default 200
+ * @memberof AnnotationAttributes.prototype
+ */
+ width: {
+ get: function () {
+ return this._width;
+ },
+ set: function (value) {
+ this._width = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates height of the callout.
+ * @type {Number}
+ * @default 100
+ * @memberof AnnotationAttributes.prototype
+ */
+ height: {
+ get: function () {
+ return this._height;
+ },
+ set: function (value) {
+ this._height = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates the radius for the corners.
+ * @type {Number}
+ * @default 0
+ * @memberof AnnotationAttributes.prototype
+ */
+ cornerRadius: {
+ get: function () {
+ return this._cornerRadius;
+ },
+ set: function (value) {
+ this._cornerRadius = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates the insets instance of this object.
+ * Insets adjusts top, bottom, left, right padding for the text.
+ * @type {Insets}
+ * @default 0, 0, 0, 0
+ * @memberof AnnotationAttributes.prototype
+ */
+ insets: {
+ get: function () {
+ return this._insets;
+ },
+ set: function (value) {
+ this._insets = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates the background color of the callout.
+ * @type {Color}
+ * @default White
+ * @memberof AnnotationAttributes.prototype
+ */
+ backgroundColor: {
+ get: function () {
+ return this._backgroundColor;
+ },
+ set: function (value) {
+ this._backgroundColor = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+
+ /**
+ * Indicates the attributes to apply to the annotation's text.
+ * @type {TextAttributes}
+ * @default The defaults of {@link TextAttributes}.
+ * @memberof AnnotationAttributes.prototype
+ */
+ textAttributes: {
+ get: function () {
+ return this._textAttributes;
+ },
+ set: function (value) {
+ this._textAttributes = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates whether to draw a leader pointing to the annotation's geographic position.
+ * @type {Boolean}
+ * @default true
+ * @memberof AnnotationAttributes.prototype
+ */
+ drawLeader: {
+ get: function () {
+ return this._drawLeader;
+ },
+ set: function (value) {
+ this._drawLeader = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates the gap width of the leader in pixels.
+ * @type {Number}
+ * @default 40
+ * @memberof AnnotationAttributes.prototype
+ */
+ leaderGapWidth: {
+ get: function () {
+ return this._leaderGapWidth;
+ },
+ set: function (value) {
+ this._leaderGapWidth = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates the gap height of the leader in pixels.
+ * @type {Number}
+ * @default 30
+ * @memberof AnnotationAttributes.prototype
+ */
+ leaderGapHeight: {
+ get: function () {
+ return this._leaderGapHeight;
+ },
+ set: function (value) {
+ this._leaderGapHeight = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates the opacity of the annotation.
+ * The value ranges from 0 to 1.
+ * Opacity affects both callout and text.
+ * @type {Number}
+ * @default 1
+ * @memberof AnnotationAttributes.prototype
+ */
+ opacity: {
+ get: function () {
+ return this._opacity;
+ },
+ set: function (value) {
+ this._opacity = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates the scale multiplier.
+ * @type {Number}
+ * @default 1
+ * @memberof AnnotationAttributes.prototype
+ */
+ scale: {
+ get: function () {
+ return this._scale;
+ },
+ set: function (value) {
+ this._scale = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * A string identifying the state of this attributes object. The string encodes the current values of all
+ * this object's properties. It's typically used to validate cached representations of shapes associated
+ * with this attributes object.
+ * @type {String}
+ * @readonly
+ * @memberof AnnotationAttributes.prototype
+ */
+ stateKey: {
+ get: function () {
+ if (this.stateKeyInvalid) {
+ this._stateKey = this.computeStateKey();
+ this.stateKeyInvalid = false;
+ }
+ return this._stateKey;
+ }
+ }
+});
+
+export default AnnotationAttributes;
diff --git a/web/test/WebWorldWind/src/shapes/Compass.js b/web/test/WebWorldWind/src/shapes/Compass.js
new file mode 100644
index 00000000..9afcde7d
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/Compass.js
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports Compass
+ */
+import ArgumentError from '../error/ArgumentError';
+import Logger from '../util/Logger';
+import Offset from '../util/Offset';
+import ScreenImage from '../shapes/ScreenImage';
+
+
+/**
+ * Constructs a compass.
+ * @alias Compass
+ * @constructor
+ * @augments ScreenImage
+ * @classdesc Displays a compass image at a specified location in the WorldWindow. The compass image rotates
+ * and tilts to reflect the current navigator's heading and tilt.
+ * @param {Offset} screenOffset The offset indicating the image's placement on the screen. If null or undefined
+ * the compass is placed at the upper-right corner of the WorldWindow.
+ * Use [the image offset property]{@link ScreenImage#imageOffset} to position the image relative to the
+ * screen point.
+ * @param {String} imagePath The URL of the image to display. If null or undefined, a default compass image is used.
+ */
+function Compass(screenOffset, imagePath) {
+
+ var sOffset = screenOffset ? screenOffset
+ : new Offset(WorldWind.OFFSET_FRACTION, 1, WorldWind.OFFSET_FRACTION, 1), // upper-right placement
+ iPath = imagePath ? imagePath : WorldWind.configuration.baseUrl + "images/notched-compass.png";
+
+ ScreenImage.call(this, sOffset, iPath);
+
+ // Must set the default image offset after calling the constructor above.
+
+ if (!screenOffset) {
+ // Align the upper right corner of the image with the screen point, and give the image some padding.
+ this.imageOffset = new Offset(WorldWind.OFFSET_FRACTION, 1.1, WorldWind.OFFSET_FRACTION, 1.1);
+ }
+
+ /**
+ * Specifies the size of the compass as a fraction of the WorldWindow width.
+ * @type {number}
+ * @default 0.15
+ */
+ this.size = 0.15;
+}
+
+Compass.prototype = Object.create(ScreenImage.prototype);
+
+/**
+ * Capture the navigator's heading and tilt and apply it to the compass' screen image.
+ * @param {DrawContext} dc The current draw context.
+ */
+Compass.prototype.render = function (dc) {
+ // Capture the navigator's heading and tilt and apply it to the compass' screen image.
+ this.imageRotation = dc.navigator.heading;
+ this.imageTilt = dc.navigator.tilt;
+
+ var t = this.getActiveTexture(dc);
+ if (t) {
+ this.imageScale = this.size * dc.currentGlContext.drawingBufferWidth / t.imageWidth;
+ }
+
+ ScreenImage.prototype.render.call(this, dc);
+};
+
+export default Compass;
diff --git a/web/test/WebWorldWind/src/shapes/GeographicMesh.js b/web/test/WebWorldWind/src/shapes/GeographicMesh.js
new file mode 100644
index 00000000..d10046a3
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/GeographicMesh.js
@@ -0,0 +1,397 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports GeographicMesh
+ */
+import AbstractMesh from '../shapes/AbstractMesh';
+import ArgumentError from '../error/ArgumentError';
+import BasicTextureProgram from '../shaders/BasicTextureProgram';
+import BoundingBox from '../geom/BoundingBox';
+import Color from '../util/Color';
+import ImageSource from '../util/ImageSource';
+import Location from '../geom/Location';
+import Logger from '../util/Logger';
+import Matrix from '../geom/Matrix';
+import PickedObject from '../pick/PickedObject';
+import Position from '../geom/Position';
+import ShapeAttributes from '../shapes/ShapeAttributes';
+import SurfacePolygon from '../shapes/SurfacePolygon';
+import Vec2 from '../geom/Vec2';
+import Vec3 from '../geom/Vec3';
+
+
+/**
+ * Constructs a geographic mesh.
+ * @alias GeographicMesh
+ * @constructor
+ * @augments AbstractMesh
+ * @classdesc Represents a 3D geographic mesh.
+ *
+ *
+ * If the latter, the mesh positions' altitudes are ignored. (If the mesh should be draped onto the
+ * terrain, you might want to use {@link SurfacePolygon} instead.)
+ *
+ *
+ *
+ *
+ * If the latter, the path positions' altitudes are ignored.
+ *
+ *
+ * @type {String}
+ * @default WorldWind.GREAT_CIRCLE
+ * @memberof Path.prototype
+ */
+ pathType: {
+ get: function () {
+ return this._pathType;
+ },
+ set: function (pathType) {
+ this._pathType = pathType;
+ this.reset();
+ }
+ },
+
+ /**
+ * Specifies whether to extrude this path to the ground by drawing a filled interior from the path to the
+ * terrain. The filled interior uses this path's interior attributes.
+ * @type {Boolean}
+ * @default false
+ * @memberof Path.prototype
+ */
+ extrude: {
+ get: function () {
+ return this._extrude;
+ },
+ set: function (extrude) {
+ this._extrude = extrude;
+ this.reset();
+ }
+ }
+});
+
+// Intentionally not documented.
+Path.prototype.determineReferencePosition = function (positions) {
+ // Assign the first position as the reference position.
+ return positions.length > 0 ? positions[0] : null;
+};
+
+// Internal. Determines whether this shape's geometry must be re-computed.
+Path.prototype.mustGenerateGeometry = function (dc) {
+ if (!this.currentData.tessellatedPoints) {
+ return true;
+ }
+
+ if (this.currentData.drawInterior !== this.activeAttributes.drawInterior
+ || this.currentData.drawVerticals !== this.activeAttributes.drawVerticals) {
+ return true;
+ }
+
+ if (!this.followTerrain && this.currentData.numSubSegments !== this.numSubSegments) {
+ return true;
+ }
+
+ if (this.followTerrain && this.currentData.terrainConformance !== this.terrainConformance) {
+ return true;
+ }
+
+ if (this.altitudeMode === WorldWind.ABSOLUTE) {
+ return false;
+ }
+
+ return this.currentData.isExpired;
+};
+
+Path.prototype.createSurfaceShape = function () {
+ return new SurfacePolyline(this.positions, null);
+};
+
+// Overridden from AbstractShape base class.
+Path.prototype.doMakeOrderedRenderable = function (dc) {
+ // A null reference position is a signal that there are no positions to render.
+ if (!this.referencePosition) {
+ return null;
+ }
+
+ // See if the current shape data can be re-used.
+ if (!this.mustGenerateGeometry(dc)) {
+ return this;
+ }
+
+ // Set the transformation matrix to correspond to the reference position.
+ var refPt = this.currentData.referencePoint;
+ dc.surfacePointForMode(this.referencePosition.latitude, this.referencePosition.longitude,
+ this.referencePosition.altitude, this._altitudeMode, refPt);
+ this.currentData.transformationMatrix.setToTranslation(refPt[0], refPt[1], refPt[2]);
+
+ // Tessellate the path in geographic coordinates.
+ var tessellatedPositions = this.makeTessellatedPositions(dc);
+ if (tessellatedPositions.length < 2) {
+ return null;
+ }
+
+ // Convert the tessellated geographic coordinates to the Cartesian coordinates that will be rendered.
+ var tessellatedPoints = this.computeRenderedPath(dc, tessellatedPositions);
+
+ this.currentData.tessellatedPoints = tessellatedPoints;
+ this.currentData.drawInterior = this.activeAttributes.drawInterior;
+ this.currentData.drawVerticals = this.activeAttributes.drawVerticals;
+ this.currentData.numSubSegments = this.numSubSegments;
+ this.currentData.terrainConformance = this.terrainConformance;
+ this.resetExpiration(this.currentData);
+ this.currentData.fillVbo = true;
+
+ // Create the extent from the Cartesian points. Those points are relative to this path's reference point, so
+ // translate the computed extent to the reference point.
+ if (!this.currentData.extent) {
+ this.currentData.extent = new BoundingBox();
+ }
+ this.currentData.extent.setToPoints(tessellatedPoints);
+ this.currentData.extent.translate(this.currentData.referencePoint);
+
+ return this;
+};
+
+// Private. Intentionally not documented.
+Path.prototype.makeTessellatedPositions = function (dc) {
+ var tessellatedPositions = [],
+ eyePoint = dc.eyePoint,
+ showVerticals = this.mustDrawVerticals(dc),
+ ptA = new Vec3(0, 0, 0),
+ ptB = new Vec3(0, 0, 0),
+ posA = this._positions[0],
+ posB, eyeDistance, pixelSize;
+
+ if (showVerticals) {
+ this.currentData.verticalIndices = new Int16Array(this.positions.length * 2);
+ this.currentData.verticalIndices[0] = 0;
+ this.currentData.verticalIndices[1] = 1;
+ }
+
+ tessellatedPositions.push(posA);
+
+ dc.surfacePointForMode(posA.latitude, posA.longitude, posA.altitude, this._altitudeMode, ptA);
+
+ for (var i = 1, len = this._positions.length; i < len; i++) {
+ posB = this._positions[i];
+ dc.surfacePointForMode(posB.latitude, posB.longitude, posB.altitude, this._altitudeMode, ptB);
+ eyeDistance = eyePoint.distanceTo(ptA);
+ pixelSize = dc.pixelSizeAtDistance(eyeDistance);
+ if (ptA.distanceTo(ptB) < pixelSize * 8 && this.altitudeMode !== WorldWind.ABSOLUTE) {
+ tessellatedPositions.push(posB); // distance is short so no need for sub-segments
+ } else {
+ this.makeSegment(dc, posA, posB, ptA, ptB, tessellatedPositions);
+ }
+
+ posA = posB;
+ ptA.copy(ptB);
+
+ if (showVerticals) {
+ var k = 2 * (tessellatedPositions.length - 1);
+ this.currentData.verticalIndices[i * 2] = k;
+ this.currentData.verticalIndices[i * 2 + 1] = k + 1;
+ }
+ }
+
+ return tessellatedPositions;
+};
+
+// Private. Intentionally not documented.
+Path.prototype.makeSegment = function (dc, posA, posB, ptA, ptB, tessellatedPositions) {
+ var eyePoint = dc.eyePoint,
+ pos = new Location(0, 0),
+ height = 0,
+ arcLength, segmentAzimuth, segmentDistance, s, p, distance;
+
+ // If it's just a straight line and not terrain following, then the segment is just two points.
+ if (this._pathType === WorldWind.LINEAR && !this._followTerrain) {
+ if (!ptA.equals(ptB)) {
+ tessellatedPositions.push(posB);
+ }
+ return;
+ }
+
+ // Compute the segment length.
+
+ if (this._pathType === WorldWind.LINEAR) {
+ segmentDistance = Location.linearDistance(posA, posB);
+ } else if (this._pathType === WorldWind.RHUMB_LINE) {
+ segmentDistance = Location.rhumbDistance(posA, posB);
+ } else {
+ segmentDistance = Location.greatCircleDistance(posA, posB);
+ }
+
+ if (this._altitudeMode !== WorldWind.CLAMP_TO_GROUND) {
+ height = 0.5 * (posA.altitude + posB.altitude);
+ }
+
+ arcLength = segmentDistance * (dc.globe.equatorialRadius + height * dc.verticalExaggeration);
+
+ if (arcLength <= 0) { // segment is 0 length
+ return;
+ }
+
+ // Compute the azimuth to apply while tessellating the segment.
+
+ if (this._pathType === WorldWind.LINEAR) {
+ segmentAzimuth = Location.linearAzimuth(posA, posB);
+ } else if (this._pathType === WorldWind.RHUMB_LINE) {
+ segmentAzimuth = Location.rhumbAzimuth(posA, posB);
+ } else {
+ segmentAzimuth = Location.greatCircleAzimuth(posA, posB);
+ }
+
+ this.scratchPoint.copy(ptA);
+ for (s = 0, p = 0; s < 1;) {
+ if (this._followTerrain) {
+ p += this._terrainConformance * dc.pixelSizeAtDistance(this.scratchPoint.distanceTo(eyePoint));
+ } else {
+ p += arcLength / this._numSubSegments;
+ }
+
+ // Stop adding intermediate positions when we reach the arc length, or the remaining distance is in
+ // millimeters on Earth.
+ if (arcLength < p || arcLength - p < 1e-9)
+ break;
+
+ s = p / arcLength;
+ distance = s * segmentDistance;
+
+ if (this._pathType === WorldWind.LINEAR) {
+ Location.linearLocation(posA, segmentAzimuth, distance, pos);
+ } else if (this._pathType === WorldWind.RHUMB_LINE) {
+ Location.rhumbLocation(posA, segmentAzimuth, distance, pos);
+ } else {
+ Location.greatCircleLocation(posA, segmentAzimuth, distance, pos);
+ }
+
+ pos.altitude = (1 - s) * posA.altitude + s * posB.altitude;
+ tessellatedPositions.push(new Position(pos.latitude, pos.longitude, pos.altitude));
+
+ if (this._followTerrain) {
+ // Compute a new reference point for eye distance.
+ dc.surfacePointForMode(pos.latitude, pos.longitude, pos.altitude,
+ WorldWind.CLAMP_TO_GROUND, this.scratchPoint);
+ }
+ }
+
+ tessellatedPositions.push(posB);
+};
+
+// Private. Intentionally not documented.
+Path.prototype.computeRenderedPath = function (dc, tessellatedPositions) {
+ var capturePoles = this.mustDrawInterior(dc) || this.mustDrawVerticals(dc),
+ eyeDistSquared = Number.MAX_VALUE,
+ eyePoint = dc.eyePoint,
+ numPoints = (capturePoles ? 2 : 1) * tessellatedPositions.length,
+ tessellatedPoints = new Float32Array(numPoints * 3),
+ stride = capturePoles ? 6 : 3,
+ pt = new Vec3(0, 0, 0),
+ altitudeMode, pos, k, dSquared;
+
+ if (this._followTerrain && this.altitudeMode !== WorldWind.CLAMP_TO_GROUND) {
+ altitudeMode = WorldWind.RELATIVE_TO_GROUND;
+ } else {
+ altitudeMode = this.altitudeMode;
+ }
+
+ for (var i = 0, len = tessellatedPositions.length; i < len; i++) {
+ pos = tessellatedPositions[i];
+
+ dc.surfacePointForMode(pos.latitude, pos.longitude, pos.altitude, altitudeMode, pt);
+
+ dSquared = pt.distanceToSquared(eyePoint);
+ if (dSquared < eyeDistSquared) {
+ eyeDistSquared = dSquared;
+ }
+
+ pt.subtract(this.currentData.referencePoint);
+
+ k = stride * i;
+ tessellatedPoints[k] = pt[0];
+ tessellatedPoints[k + 1] = pt[1];
+ tessellatedPoints[k + 2] = pt[2];
+
+ if (capturePoles) {
+ dc.surfacePointForMode(pos.latitude, pos.longitude, 0, WorldWind.CLAMP_TO_GROUND, pt);
+
+ dSquared = pt.distanceToSquared(eyePoint);
+ if (dSquared < eyeDistSquared) {
+ eyeDistSquared = dSquared;
+ }
+
+ pt.subtract(this.currentData.referencePoint);
+
+ tessellatedPoints[k + 3] = pt[0];
+ tessellatedPoints[k + 4] = pt[1];
+ tessellatedPoints[k + 5] = pt[2];
+ }
+ }
+
+ this.currentData.pointBufferHasExtrusionPoints = capturePoles;
+ this.currentData.eyeDistance = Math.sqrt(eyeDistSquared);
+
+ return tessellatedPoints;
+};
+
+// Private. Intentionally not documented.
+Path.prototype.mustDrawInterior = function (dc) {
+ return this.activeAttributes.drawInterior
+ && this._extrude
+ && this._altitudeMode !== WorldWind.CLAMP_TO_GROUND;
+};
+
+// Private. Intentionally not documented.
+Path.prototype.mustDrawVerticals = function (dc) {
+ return this.activeAttributes.drawOutline && this.activeAttributes.drawVerticals
+ && this.altitudeMode !== WorldWind.CLAMP_TO_GROUND;
+};
+
+// Overridden from AbstractShape base class.
+Path.prototype.doRenderOrdered = function (dc) {
+ var gl = dc.currentGlContext,
+ program = dc.currentProgram,
+ currentData = this.currentData,
+ numPoints = currentData.tessellatedPoints.length / 3,
+ vboId, color, pickColor, stride, nPts;
+
+ this.applyMvpMatrix(dc);
+
+ if (!currentData.vboCacheKey) {
+ currentData.vboCacheKey = dc.gpuResourceCache.generateCacheKey();
+ }
+
+ vboId = dc.gpuResourceCache.resourceForKey(currentData.vboCacheKey);
+ if (!vboId) {
+ vboId = gl.createBuffer();
+ dc.gpuResourceCache.putResource(this.currentData.vboCacheKey, vboId,
+ currentData.tessellatedPoints.length * 4);
+ currentData.fillVbo = true;
+ }
+
+ // Bind and if necessary fill the VBO. We fill the VBO here rather than in doMakeOrderedRenderable so that
+ // there's no possibility of the VBO being ejected from the cache between the time it's filled and
+ // the time it's used.
+ gl.bindBuffer(gl.ARRAY_BUFFER, vboId);
+ if (currentData.fillVbo) {
+ gl.bufferData(gl.ARRAY_BUFFER, currentData.tessellatedPoints,
+ gl.STATIC_DRAW);
+ dc.frameStatistics.incrementVboLoadCount(1);
+ }
+
+ program.loadTextureEnabled(gl, false);
+
+ if (dc.pickingMode) {
+ pickColor = dc.uniquePickColor();
+ }
+
+ if (this.mustDrawInterior(dc)) {
+ color = this.activeAttributes.interiorColor;
+ // Disable writing the shape's fragments to the depth buffer when the interior is semi-transparent.
+ gl.depthMask(color.alpha * this.layer.opacity >= 1 || dc.pickingMode);
+ program.loadColor(gl, dc.pickingMode ? pickColor : color);
+ program.loadOpacity(gl, dc.pickingMode ? 1 : this.layer.opacity);
+
+ gl.vertexAttribPointer(program.vertexPointLocation, 3, gl.FLOAT, false, 0, 0);
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, numPoints);
+ }
+
+ if (this.activeAttributes.drawOutline) {
+ if (this.mustDrawVerticals(dc) && this.mustDrawInterior(dc)
+ || this.altitudeMode === WorldWind.CLAMP_TO_GROUND) {
+ // Make the verticals stand out from the interior, or the outline stand out from the terrain.
+ this.applyMvpMatrixForOutline(dc);
+ }
+
+ color = this.activeAttributes.outlineColor;
+ // Disable writing the shape's fragments to the depth buffer when the interior is semi-transparent.
+ gl.depthMask(color.alpha * this.layer.opacity >= 1 || dc.pickingMode);
+ program.loadColor(gl, dc.pickingMode ? pickColor : color);
+ program.loadOpacity(gl, dc.pickingMode ? 1 : this.layer.opacity);
+
+ gl.lineWidth(this.activeAttributes.outlineWidth);
+
+ if (this.currentData.pointBufferHasExtrusionPoints) {
+ stride = 24;
+ nPts = numPoints / 2;
+ } else {
+ stride = 12;
+ nPts = numPoints;
+ }
+
+ gl.vertexAttribPointer(program.vertexPointLocation, 3, gl.FLOAT, false, stride, 0);
+ gl.drawArrays(gl.LINE_STRIP, 0, nPts);
+
+ if (this.mustDrawVerticals(dc)) {
+ if (!currentData.verticalIndicesVboCacheKey) {
+ currentData.verticalIndicesVboCacheKey = dc.gpuResourceCache.generateCacheKey();
+ }
+
+ vboId = dc.gpuResourceCache.resourceForKey(currentData.verticalIndicesVboCacheKey);
+ if (!vboId) {
+ vboId = gl.createBuffer();
+ dc.gpuResourceCache.putResource(currentData.verticalIndicesVboCacheKey, vboId,
+ currentData.verticalIndices.length * 4);
+ currentData.fillVbo = true;
+ }
+
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, vboId);
+ if (currentData.fillVbo) {
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, currentData.verticalIndices,
+ gl.STATIC_DRAW);
+ dc.frameStatistics.incrementVboLoadCount(1);
+ }
+
+ gl.vertexAttribPointer(program.vertexPointLocation, 3, gl.FLOAT, false, 0, 0);
+ gl.drawElements(gl.LINES, currentData.verticalIndices.length,
+ gl.UNSIGNED_SHORT, 0);
+ }
+ }
+ currentData.fillVbo = false;
+
+ if (dc.pickingMode) {
+ var po = new PickedObject(pickColor, this.pickDelegate ? this.pickDelegate : this, null, this.layer,
+ false);
+ dc.resolvePick(po);
+ }
+};
+
+// Overridden from AbstractShape base class.
+Path.prototype.beginDrawing = function (dc) {
+ var gl = dc.currentGlContext;
+
+ if (this.mustDrawInterior(dc)) {
+ gl.disable(gl.CULL_FACE);
+ }
+
+ dc.findAndBindProgram(BasicTextureProgram);
+ gl.enableVertexAttribArray(dc.currentProgram.vertexPointLocation);
+};
+
+// Overridden from AbstractShape base class.
+Path.prototype.endDrawing = function (dc) {
+ var gl = dc.currentGlContext;
+
+ gl.disableVertexAttribArray(dc.currentProgram.vertexPointLocation);
+ gl.depthMask(true);
+ gl.lineWidth(1);
+ gl.enable(gl.CULL_FACE);
+};
+
+export default Path;
diff --git a/web/test/WebWorldWind/src/shapes/Placemark.js b/web/test/WebWorldWind/src/shapes/Placemark.js
new file mode 100644
index 00000000..863a21ca
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/Placemark.js
@@ -0,0 +1,793 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports Placemark
+ */
+import ArgumentError from '../error/ArgumentError';
+import BasicTextureProgram from '../shaders/BasicTextureProgram';
+import Color from '../util/Color';
+import Font from '../util/Font';
+import Logger from '../util/Logger';
+import Matrix from '../geom/Matrix';
+import PickedObject from '../pick/PickedObject';
+import PlacemarkAttributes from '../shapes/PlacemarkAttributes';
+import Renderable from '../render/Renderable';
+import Vec2 from '../geom/Vec2';
+import Vec3 from '../geom/Vec3';
+import WWMath from '../util/WWMath';
+
+
+/**
+ * Constructs a placemark.
+ * @alias Placemark
+ * @constructor
+ * @augments Renderable
+ * @classdesc Represents a Placemark shape. A placemark displays an image, a label and a leader line connecting
+ * the placemark's geographic position to the ground. All three of these items are optional. By default, the
+ * leader line is not pickable. See [enableLeaderLinePicking]{@link Placemark#enableLeaderLinePicking}.
+ *
+ *
+ * @default WorldWind.ABSOLUTE
+ */
+ this.altitudeMode = WorldWind.ABSOLUTE;
+
+ /**
+ * Indicates whether this placemark has visual priority over other shapes in the scene.
+ * @type {Boolean}
+ * @default false
+ */
+ this.alwaysOnTop = false;
+
+ /**
+ * Indicates whether this placemark's leader line, if any, is pickable.
+ * @type {Boolean}
+ * @default false
+ */
+ this.enableLeaderLinePicking = false;
+
+ /**
+ * Indicates whether this placemark's image should be re-retrieved even if it has already been retrieved.
+ * Set this property to true when the image has changed but has the same image path.
+ * The property is set to false when the image is re-retrieved.
+ * @type {Boolean}
+ */
+ this.updateImage = true;
+
+ /**
+ * Indicates the group ID of the declutter group to include this placemark. If non-zero, this placemark
+ * is decluttered relative to all other shapes within its group.
+ * @type {Number}
+ * @default 2
+ */
+ this.declutterGroup = 2;
+
+ /**
+ * This placemark's target label visibility, a value between 0 and 1. During ordered rendering this
+ * placemark modifies its [current visibility]{@link Placemark#currentVisibility} towards its target
+ * visibility at the rate specified by the draw context's [fade time]{@link DrawContext#fadeTime} property.
+ * The target visibility and current visibility are used to control the fading in and out of this
+ * placemark's label.
+ * @type {Number}
+ * @default 1
+ */
+ this.targetVisibility = 1;
+
+ /**
+ * This placemark's current label visibility, a value between 0 and 1. This property scales the placemark's
+ * effective label opacity. It is incremented or decremented each frame according to the draw context's
+ * [fade time]{@link DrawContext#fadeTime} property in order to achieve this placemark's
+ * [target visibility]{@link Placemark#targetVisibility}. This current visibility and target visibility are
+ * used to control the fading in and out of this placemark's label.
+ * @type {Number}
+ * @default 1
+ * @readonly
+ */
+ this.currentVisibility = 1;
+
+ /**
+ * The amount of rotation to apply to the image, measured in degrees clockwise and relative to this
+ * placemark's [imageRotationReference]{@link Placemark#imageRotationReference}.
+ * @type {Number}
+ * @default 0
+ */
+ this.imageRotation = 0;
+
+ /**
+ * The amount of tilt to apply to the image, measured in degrees away from the eye point and relative
+ * to this placemark's [imageTiltReference]{@link Placemark#imageTiltReference}. While any positive or
+ * negative number may be specified, values outside the range [0. 90] cause some or all of the image to
+ * be clipped.
+ * @type {Number}
+ * @default 0
+ */
+ this.imageTilt = 0;
+
+ /**
+ * Indicates whether to apply this placemark's image rotation relative to the screen or the globe.
+ * If WorldWind.RELATIVE_TO_SCREEN, this placemark's image is rotated in the plane of the screen and
+ * its orientation relative to the globe changes as the view changes.
+ * If WorldWind.RELATIVE_TO_GLOBE, this placemark's image is rotated in a plane tangent to the globe
+ * at this placemark's position and retains its orientation relative to the globe.
+ * @type {String}
+ * @default WorldWind.RELATIVE_TO_SCREEN
+ */
+ this.imageRotationReference = WorldWind.RELATIVE_TO_SCREEN;
+
+ /**
+ * Indicates whether to apply this placemark's image tilt relative to the screen or the globe.
+ * If WorldWind.RELATIVE_TO_SCREEN, this placemark's image is tilted inwards (for positive tilts)
+ * relative to the plane of the screen, and its orientation relative to the globe changes as the view
+ * changes. If WorldWind.RELATIVE_TO_GLOBE, this placemark's image is tilted towards the globe's surface,
+ * and retains its orientation relative to the surface.
+ * @type {string}
+ * @default WorldWind.RELATIVE_TO_SCREEN
+ */
+ this.imageTiltReference = WorldWind.RELATIVE_TO_SCREEN;
+
+ // Internal use only. Intentionally not documented.
+ this.activeAttributes = null;
+
+ // Internal use only. Intentionally not documented.
+ this.activeTexture = null;
+
+ // Internal use only. Intentionally not documented.
+ this.labelTexture = null;
+
+ // Internal use only. Intentionally not documented.
+ this.placePoint = new Vec3(0, 0, 0); // Cartesian point corresponding to this placemark's geographic position
+
+ // Internal use only. Intentionally not documented.
+ this.groundPoint = new Vec3(0, 0, 0); // Cartesian point corresponding to ground position below this placemark
+
+ // Internal use only. Intentionally not documented.
+ this.imageTransform = Matrix.fromIdentity();
+
+ // Internal use only. Intentionally not documented.
+ this.labelTransform = Matrix.fromIdentity();
+
+ // Internal use only. Intentionally not documented.
+ this.texCoordMatrix = Matrix.fromIdentity();
+
+ // Internal use only. Intentionally not documented.
+ this.imageBounds = null;
+
+ // Internal use only. Intentionally not documented.
+ this.layer = null;
+
+ // Internal use only. Intentionally not documented.
+ this.depthOffset = -0.003;
+}
+
+// Internal use only. Intentionally not documented.
+Placemark.screenPoint = new Vec3(0, 0, 0); // scratch variable
+Placemark.matrix = Matrix.fromIdentity(); // scratch variable
+Placemark.scratchPoint = new Vec3(0, 0, 0); // scratch variable
+
+Placemark.prototype = Object.create(Renderable.prototype);
+
+Object.defineProperties(Placemark.prototype, {
+ /**
+ * Indicates the screen coordinate bounds of this shape during ordered rendering.
+ * @type {Rectangle}
+ * @readonly
+ * @memberof Placemark.prototype
+ */
+ screenBounds: {
+ get: function () {
+ return this.labelBounds;
+ }
+ }
+});
+
+/**
+ * Copies the contents of a specified placemark to this placemark.
+ * @param {Placemark} that The placemark to copy.
+ */
+Placemark.prototype.copy = function (that) {
+ this.position = that.position;
+ this.attributes = that.attributes;
+ this.highlightAttributes = that.highlightAttributes;
+ this.highlighted = that.highlighted;
+ this.enabled = that.enabled;
+ this.label = that.label;
+ this.altitudeMode = that.altitudeMode;
+ this.pickDelegate = that.pickDelegate;
+ this.alwaysOnTop = that.alwaysOnTop;
+ this.depthOffset = that.depthOffset;
+ this.targetVisibility = that.targetVisibility;
+ this.currentVisibility = that.currentVisibility;
+ this.imageRotation = that.imageRotation;
+ this.imageTilt = that.imageTilt;
+ this.imageRotationReference = that.imageRotationReference;
+ this.imageTiltReference = that.imageTiltReference;
+
+ return this;
+};
+
+/**
+ * Creates a new placemark that is a copy of this placemark.
+ * @returns {Placemark} The new placemark.
+ */
+Placemark.prototype.clone = function () {
+ var clone = new Placemark(this.position);
+
+ clone.copy(this);
+ clone.pickDelegate = this.pickDelegate ? this.pickDelegate : this;
+
+ return clone;
+};
+
+/**
+ * Renders this placemark. This method is typically not called by applications but is called by
+ * {@link RenderableLayer} during rendering. For this shape this method creates and
+ * enques an ordered renderable with the draw context and does not actually draw the placemark.
+ * @param {DrawContext} dc The current draw context.
+ */
+Placemark.prototype.render = function (dc) {
+ if (!this.enabled) {
+ return;
+ }
+
+ if (!dc.accumulateOrderedRenderables) {
+ return;
+ }
+
+ if (dc.globe.projectionLimits
+ && !dc.globe.projectionLimits.containsLocation(this.position.latitude, this.position.longitude)) {
+ return;
+ }
+
+ // Create an ordered renderable for this placemark. If one has already been created this frame then we're
+ // in 2D-continuous mode and another needs to be created for one of the alternate globe offsets.
+ var orderedPlacemark;
+ if (this.lastFrameTime !== dc.timestamp) {
+ orderedPlacemark = this.makeOrderedRenderable(dc);
+ } else {
+ var placemarkCopy = this.clone();
+ orderedPlacemark = placemarkCopy.makeOrderedRenderable(dc);
+ }
+
+ if (!orderedPlacemark) {
+ return;
+ }
+
+ if (!orderedPlacemark.isVisible(dc)) {
+ return;
+ }
+
+ orderedPlacemark.layer = dc.currentLayer;
+
+ this.lastFrameTime = dc.timestamp;
+ dc.addOrderedRenderable(orderedPlacemark);
+};
+
+/**
+ * Draws this shape as an ordered renderable. Applications do not call this function. It is called by
+ * [WorldWindow]{@link WorldWindow} during rendering.
+ * @param {DrawContext} dc The current draw context.
+ */
+Placemark.prototype.renderOrdered = function (dc) {
+ this.drawOrderedPlacemark(dc);
+
+ if (dc.pickingMode) {
+ var po = new PickedObject(this.pickColor.clone(), this.pickDelegate ? this.pickDelegate : this,
+ this.position, this.layer, false);
+
+ if (dc.pickPoint && this.mustDrawLabel()) {
+ if (this.labelBounds.containsPoint(
+ dc.convertPointToViewport(dc.pickPoint, Placemark.scratchPoint))) {
+ po.labelPicked = true;
+ }
+ }
+ dc.resolvePick(po);
+ }
+};
+
+/* INTENTIONALLY NOT DOCUMENTED
+ * Creates an ordered renderable for this shape.
+ * @protected
+ * @param {DrawContext} dc The current draw context.
+ * @returns {OrderedRenderable} The ordered renderable. May be null, in which case an ordered renderable
+ * cannot be created or should not be created at the time this method is called.
+ */
+Placemark.prototype.makeOrderedRenderable = function (dc) {
+ var w, h, s,
+ offset;
+
+ this.determineActiveAttributes(dc);
+ if (!this.activeAttributes) {
+ return null;
+ }
+
+ // Compute the placemark's model point and corresponding distance to the eye point. If the placemark's
+ // position is terrain-dependent but off the terrain, then compute it ABSOLUTE so that we have a point for
+ // the placemark and are thus able to draw it. Otherwise its image and label portion that are potentially
+ // over the terrain won't get drawn, and would disappear as soon as there is no terrain at the placemark's
+ // position. This can occur at the window edges.
+ dc.surfacePointForMode(this.position.latitude, this.position.longitude, this.position.altitude,
+ this.altitudeMode, this.placePoint);
+
+ this.eyeDistance = this.alwaysOnTop ? 0 : dc.eyePoint.distanceTo(this.placePoint);
+
+ if (this.mustDrawLeaderLine(dc)) {
+ dc.surfacePointForMode(this.position.latitude, this.position.longitude, 0,
+ this.altitudeMode, this.groundPoint);
+ }
+
+ // Compute the placemark's screen point in the OpenGL coordinate system of the WorldWindow by projecting its model
+ // coordinate point onto the viewport. Apply a depth offset in order to cause the placemark to appear above nearby
+ // terrain. When a placemark is displayed near the terrain portions of its geometry are often behind the terrain,
+ // yet as a screen element the placemark is expected to be visible. We adjust its depth values rather than moving
+ // the placemark itself to avoid obscuring its actual position.
+ if (!dc.projectWithDepth(this.placePoint, this.depthOffset, Placemark.screenPoint)) {
+ return null;
+ }
+
+ var visibilityScale = this.eyeDistanceScaling ?
+ Math.max(0.0, Math.min(1, this.eyeDistanceScalingThreshold / this.eyeDistance)) : 1;
+
+ // Compute the placemark's transform matrix and texture coordinate matrix according to its screen point, image size,
+ // image offset and image scale. The image offset is defined with its origin at the image's bottom-left corner and
+ // axes that extend up and to the right from the origin point. When the placemark has no active texture the image
+ // scale defines the image size and no other scaling is applied.
+ if (this.activeTexture) {
+ w = this.activeTexture.originalImageWidth;
+ h = this.activeTexture.originalImageHeight;
+ s = this.activeAttributes.imageScale * visibilityScale;
+ offset = this.activeAttributes.imageOffset.offsetForSize(w, h);
+
+ this.imageTransform.setTranslation(
+ Placemark.screenPoint[0] - offset[0] * s,
+ Placemark.screenPoint[1] - offset[1] * s,
+ Placemark.screenPoint[2]);
+
+ this.imageTransform.setScale(w * s, h * s, 1);
+ } else {
+ s = this.activeAttributes.imageScale * visibilityScale;
+ offset = this.activeAttributes.imageOffset.offsetForSize(s, s);
+
+ this.imageTransform.setTranslation(
+ Placemark.screenPoint[0] - offset[0],
+ Placemark.screenPoint[1] - offset[1],
+ Placemark.screenPoint[2]);
+
+ this.imageTransform.setScale(s, s, 1);
+ }
+
+ this.imageBounds = WWMath.boundingRectForUnitQuad(this.imageTransform);
+
+ // If there's a label, perform these same operations for the label texture.
+
+ if (this.mustDrawLabel()) {
+
+ this.labelTexture = dc.createTextTexture(this.label, this.activeAttributes.labelAttributes);
+
+ w = this.labelTexture.imageWidth;
+ h = this.labelTexture.imageHeight;
+ s = this.activeAttributes.labelAttributes.scale * visibilityScale;
+ offset = this.activeAttributes.labelAttributes.offset.offsetForSize(w, h);
+
+ this.labelTransform.setTranslation(
+ Placemark.screenPoint[0] - offset[0] * s,
+ Placemark.screenPoint[1] - offset[1] * s,
+ Placemark.screenPoint[2]);
+
+ this.labelTransform.setScale(w * s, h * s, 1);
+
+ this.labelBounds = WWMath.boundingRectForUnitQuad(this.labelTransform);
+ }
+
+ return this;
+};
+
+// Internal. Intentionally not documented.
+Placemark.prototype.determineActiveAttributes = function (dc) {
+ if (this.highlighted && this.highlightAttributes) {
+ this.activeAttributes = this.highlightAttributes;
+ } else {
+ this.activeAttributes = this.attributes;
+ }
+
+ if (this.activeAttributes && this.activeAttributes.imageSource) {
+ this.activeTexture = dc.gpuResourceCache.resourceForKey(this.activeAttributes.imageSource);
+
+ if (!this.activeTexture || this.updateImage) {
+ this.activeTexture = dc.gpuResourceCache.retrieveTexture(dc.currentGlContext,
+ this.activeAttributes.imageSource);
+ this.updateImage = false;
+ }
+ }
+};
+
+// Internal. Intentionally not documented.
+Placemark.prototype.isVisible = function (dc) {
+ if (dc.pickingMode) {
+ return dc.pickRectangle && (this.imageBounds.intersects(dc.pickRectangle)
+ || this.mustDrawLabel() && this.labelBounds.intersects(dc.pickRectangle)
+ || this.mustDrawLeaderLine(dc)
+ && dc.pickFrustum.intersectsSegment(this.groundPoint, this.placePoint));
+ } else {
+ return this.imageBounds.intersects(dc.viewport)
+ || this.mustDrawLabel() && this.labelBounds.intersects(dc.viewport)
+ || this.mustDrawLeaderLine(dc)
+ && dc.frustumInModelCoordinates.intersectsSegment(this.groundPoint, this.placePoint);
+ }
+};
+
+// Internal. Intentionally not documented.
+Placemark.prototype.drawOrderedPlacemark = function (dc) {
+ this.beginDrawing(dc);
+
+ try {
+ this.doDrawOrderedPlacemark(dc);
+ if (!dc.pickingMode) {
+ this.drawBatchOrderedPlacemarks(dc);
+ }
+ } finally {
+ this.endDrawing(dc);
+ }
+};
+
+// Internal. Intentionally not documented.
+Placemark.prototype.drawBatchOrderedPlacemarks = function (dc) {
+ // Draw any subsequent placemarks in the ordered renderable queue, removing each from the queue as it's
+ // processed. This avoids the overhead of setting up and tearing down OpenGL state for each placemark.
+
+ var or;
+
+ while ((or = dc.peekOrderedRenderable()) && or.doDrawOrderedPlacemark) {
+ dc.popOrderedRenderable(); // remove it from the queue
+
+ try {
+ or.doDrawOrderedPlacemark(dc);
+ } catch (e) {
+ Logger.logMessage(Logger.LEVEL_WARNING, 'Placemark', 'drawBatchOrderedPlacemarks',
+ "Error occurred while rendering placemark using batching: " + e.message);
+ }
+ // Keep going. Render the rest of the ordered renderables.
+ }
+};
+
+// Internal. Intentionally not documented.
+Placemark.prototype.beginDrawing = function (dc) {
+ var gl = dc.currentGlContext,
+ program;
+
+ dc.findAndBindProgram(BasicTextureProgram);
+
+ // Configure GL to use the draw context's unit quad VBOs for both model coordinates and texture coordinates.
+ // Most browsers can share the same buffer for vertex and texture coordinates, but Internet Explorer requires
+ // that they be in separate buffers, so the code below uses the 3D buffer for vertex coords and the 2D
+ // buffer for texture coords.
+ program = dc.currentProgram;
+ gl.bindBuffer(gl.ARRAY_BUFFER, dc.unitQuadBuffer());
+ gl.vertexAttribPointer(program.vertexTexCoordLocation, 2, gl.FLOAT, false, 0, 0);
+ gl.enableVertexAttribArray(program.vertexPointLocation);
+ gl.enableVertexAttribArray(program.vertexTexCoordLocation);
+
+ // Tell the program which texture unit to use.
+ program.loadTextureUnit(gl, gl.TEXTURE0);
+ program.loadModulateColor(gl, dc.pickingMode);
+};
+
+// Internal. Intentionally not documented.
+Placemark.prototype.endDrawing = function (dc) {
+ var gl = dc.currentGlContext,
+ program = dc.currentProgram;
+
+ // Clear the vertex attribute state.
+ gl.disableVertexAttribArray(program.vertexPointLocation);
+ gl.disableVertexAttribArray(program.vertexTexCoordLocation);
+
+ // Clear GL bindings.
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
+ gl.bindTexture(gl.TEXTURE_2D, null);
+};
+
+// Internal. Intentionally not documented.
+Placemark.prototype.doDrawOrderedPlacemark = function (dc) {
+ var gl = dc.currentGlContext,
+ program = dc.currentProgram,
+ depthTest = true,
+ textureBound;
+
+ if (dc.pickingMode) {
+ this.pickColor = dc.uniquePickColor();
+ }
+
+ if (this.eyeDistanceScaling && this.eyeDistance > this.eyeDistanceScalingLabelThreshold) {
+ // Target visibility is set to 0 to cause the label to be faded in or out. Nothing else
+ // here uses target visibility.
+ this.targetVisibility = 0;
+ }
+
+ // Compute the effective visibility. Use the current value if picking.
+ if (!dc.pickingMode && this.mustDrawLabel()) {
+ if (this.currentVisibility != this.targetVisibility) {
+ var visibilityDelta = (dc.timestamp - dc.previousRedrawTimestamp) / dc.fadeTime;
+ if (this.currentVisibility < this.targetVisibility) {
+ this.currentVisibility = Math.min(1, this.currentVisibility + visibilityDelta);
+ } else {
+ this.currentVisibility = Math.max(0, this.currentVisibility - visibilityDelta);
+ }
+ dc.redrawRequested = true;
+ }
+ }
+
+ program.loadOpacity(gl, dc.pickingMode ? 1 : this.layer.opacity);
+
+ // Draw the leader line first so that the image and label have visual priority.
+ if (this.mustDrawLeaderLine(dc)) {
+ if (!this.leaderLinePoints) {
+ this.leaderLinePoints = new Float32Array(6);
+ }
+
+ this.leaderLinePoints[0] = this.groundPoint[0]; // computed during makeOrderedRenderable
+ this.leaderLinePoints[1] = this.groundPoint[1];
+ this.leaderLinePoints[2] = this.groundPoint[2];
+ this.leaderLinePoints[3] = this.placePoint[0]; // computed during makeOrderedRenderable
+ this.leaderLinePoints[4] = this.placePoint[1];
+ this.leaderLinePoints[5] = this.placePoint[2];
+
+ if (!this.leaderLineCacheKey) {
+ this.leaderLineCacheKey = dc.gpuResourceCache.generateCacheKey();
+ }
+
+ var leaderLineVboId = dc.gpuResourceCache.resourceForKey(this.leaderLineCacheKey);
+ if (!leaderLineVboId) {
+ leaderLineVboId = gl.createBuffer();
+ dc.gpuResourceCache.putResource(this.leaderLineCacheKey, leaderLineVboId,
+ this.leaderLinePoints.length * 4);
+ }
+
+ program.loadTextureEnabled(gl, false);
+ program.loadColor(gl, dc.pickingMode ? this.pickColor :
+ this.activeAttributes.leaderLineAttributes.outlineColor);
+
+ Placemark.matrix.copy(dc.modelviewProjection);
+ program.loadModelviewProjection(gl, Placemark.matrix);
+
+ if (!this.activeAttributes.leaderLineAttributes.depthTest) {
+ gl.disable(gl.DEPTH_TEST);
+ }
+
+ gl.lineWidth(this.activeAttributes.leaderLineAttributes.outlineWidth);
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, leaderLineVboId);
+ gl.bufferData(gl.ARRAY_BUFFER, this.leaderLinePoints, gl.STATIC_DRAW);
+ dc.frameStatistics.incrementVboLoadCount(1);
+ gl.vertexAttribPointer(program.vertexPointLocation, 3, gl.FLOAT, false, 0, 0);
+ gl.drawArrays(gl.LINES, 0, 2);
+ }
+
+ // Turn off depth testing for the placemark image if requested. The placemark label and leader line have
+ // their own depth-test controls.
+ if (!this.activeAttributes.depthTest) {
+ depthTest = false;
+ gl.disable(gl.DEPTH_TEST);
+ }
+
+ // Suppress frame buffer writes for the placemark image and its label.
+ // tag, 6/17/15: It's not clear why this call was here. It was carried over from WWJ.
+ //gl.depthMask(false);
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, dc.unitQuadBuffer3());
+ gl.vertexAttribPointer(program.vertexPointLocation, 3, gl.FLOAT, false, 0, 0);
+
+ // Compute and specify the MVP matrix.
+ Placemark.matrix.copy(dc.screenProjection);
+ Placemark.matrix.multiplyMatrix(this.imageTransform);
+
+ var actualRotation = this.imageRotationReference === WorldWind.RELATIVE_TO_GLOBE ?
+ dc.navigator.heading - this.imageRotation : -this.imageRotation;
+ Placemark.matrix.multiplyByTranslation(0.5, 0.5, 0);
+ Placemark.matrix.multiplyByRotation(0, 0, 1, actualRotation);
+ Placemark.matrix.multiplyByTranslation(-0.5, -0.5, 0);
+
+ // Perform the tilt before applying the rotation so that the image tilts back from its base into
+ // the view volume.
+ var actualTilt = this.imageTiltReference === WorldWind.RELATIVE_TO_GLOBE ?
+ dc.navigator.tilt + this.imageTilt : this.imageTilt;
+ Placemark.matrix.multiplyByRotation(-1, 0, 0, actualTilt);
+
+ program.loadModelviewProjection(gl, Placemark.matrix);
+
+ // Enable texture for both normal display and for picking. If picking is enabled in the shader (set in
+ // beginDrawing() above) then the texture's alpha component is still needed in order to modulate the
+ // pick color to mask off transparent pixels.
+ program.loadTextureEnabled(gl, true);
+
+ if (dc.pickingMode) {
+ program.loadColor(gl, this.pickColor);
+ } else {
+ program.loadColor(gl, this.activeAttributes.imageColor);
+ }
+
+ this.texCoordMatrix.setToIdentity();
+ if (this.activeTexture) {
+ this.texCoordMatrix.multiplyByTextureTransform(this.activeTexture);
+ }
+ program.loadTextureMatrix(gl, this.texCoordMatrix);
+
+ if (this.activeTexture) {
+ textureBound = this.activeTexture.bind(dc); // returns false if active texture is null or cannot be bound
+ program.loadTextureEnabled(gl, textureBound);
+ } else {
+ program.loadTextureEnabled(gl, false);
+ }
+
+ // Draw the placemark's image quad.
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+
+ if (this.mustDrawLabel() && this.currentVisibility > 0) {
+ program.loadOpacity(gl, dc.pickingMode ? 1 : this.layer.opacity * this.currentVisibility);
+
+ Placemark.matrix.copy(dc.screenProjection);
+ Placemark.matrix.multiplyMatrix(this.labelTransform);
+ program.loadModelviewProjection(gl, Placemark.matrix);
+
+ if (!dc.pickingMode && this.labelTexture) {
+ this.texCoordMatrix.setToIdentity();
+ this.texCoordMatrix.multiplyByTextureTransform(this.labelTexture);
+
+ program.loadTextureMatrix(gl, this.texCoordMatrix);
+ program.loadColor(gl, Color.WHITE);
+
+ textureBound = this.labelTexture.bind(dc);
+ program.loadTextureEnabled(gl, textureBound);
+ } else {
+ program.loadTextureEnabled(gl, false);
+ program.loadColor(gl, this.pickColor);
+ }
+
+ if (this.activeAttributes.labelAttributes.depthTest) {
+ if (!depthTest) {
+ depthTest = true;
+ gl.enable(gl.DEPTH_TEST);
+ }
+ } else {
+ depthTest = false;
+ gl.disable(gl.DEPTH_TEST);
+ }
+
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+ }
+
+ if (!depthTest) {
+ gl.enable(gl.DEPTH_TEST);
+ }
+
+ // tag, 6/17/15: See note on depthMask above in this function.
+ //gl.depthMask(true);
+};
+
+// Internal. Intentionally not documented.
+Placemark.prototype.mustDrawLabel = function () {
+ return this.label && this.label.length > 0 && this.activeAttributes.labelAttributes;
+};
+
+// Internal. Intentionally not documented.
+Placemark.prototype.mustDrawLeaderLine = function (dc) {
+ return this.activeAttributes.drawLeaderLine && this.activeAttributes.leaderLineAttributes
+ && (!dc.pickingMode || this.enableLeaderLinePicking);
+};
+
+// Internal use only. Intentionally not documented.
+Placemark.prototype.getReferencePosition = function () {
+ return this.position;
+};
+
+// Internal use only. Intentionally not documented.
+Placemark.prototype.moveTo = function (globe, position) {
+ this.position = position;
+};
+
+export default Placemark;
diff --git a/web/test/WebWorldWind/src/shapes/PlacemarkAttributes.js b/web/test/WebWorldWind/src/shapes/PlacemarkAttributes.js
new file mode 100644
index 00000000..bc5e511c
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/PlacemarkAttributes.js
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports PlacemarkAttributes
+ */
+import Color from '../util/Color';
+import Font from '../util/Font';
+import Offset from '../util/Offset';
+import ShapeAttributes from '../shapes/ShapeAttributes';
+import TextAttributes from '../shapes/TextAttributes';
+
+
+/**
+ * Constructs a placemark attributes bundle.
+ * The defaults indicate a placemark displayed as a white 1x1 pixel square centered on the placemark's
+ * geographic position.
+ * @alias PlacemarkAttributes
+ * @constructor
+ * @classdesc Holds attributes applied to {@link Placemark} shapes.
+ *
+ * @param {PlacemarkAttributes} attributes Attributes to initialize this attributes instance to. May be null,
+ * in which case the new instance contains default attributes.
+ */
+function PlacemarkAttributes(attributes) {
+ // These are all documented with their property accessors below.
+ this._imageColor = attributes ? attributes._imageColor.clone() : Color.WHITE.clone();
+ this._imageOffset = attributes ? attributes._imageOffset
+ : new Offset(WorldWind.OFFSET_FRACTION, 0.5, WorldWind.OFFSET_FRACTION, 0.5);
+ this._imageScale = attributes ? attributes._imageScale : 1;
+ this._imageSource = attributes ? attributes._imageSource : null;
+ this._depthTest = attributes ? attributes._depthTest : true;
+ this._labelAttributes = attributes ? attributes._labelAttributes : new TextAttributes(null);
+ this._drawLeaderLine = attributes ? attributes._drawLeaderLine : false;
+ this._leaderLineAttributes = attributes ? attributes._leaderLineAttributes : new ShapeAttributes(null);
+
+ /**
+ * Indicates whether this object's state key is invalid. Subclasses must set this value to true when their
+ * attributes change. The state key will be automatically computed the next time it's requested. This flag
+ * will be set to false when that occurs.
+ * @type {Boolean}
+ * @protected
+ */
+ this.stateKeyInvalid = true;
+}
+
+/**
+ * Computes the state key for this attributes object. Subclasses that define additional attributes must
+ * override this method, call it from that method, and append the state of their attributes to its
+ * return value.
+ * @returns {String} The state key for this object.
+ * @protected
+ */
+PlacemarkAttributes.prototype.computeStateKey = function () {
+ return "ic " + this._imageColor.toHexString(true)
+ + " io " + this._imageOffset.toString()
+ + " is " + this._imageScale
+ + " ip " + this._imageSource
+ + " dt " + this._depthTest
+ + " la " + this._labelAttributes.stateKey
+ + " dll " + this._drawLeaderLine
+ + " lla " + this._leaderLineAttributes.stateKey;
+};
+
+Object.defineProperties(PlacemarkAttributes.prototype, {
+ /**
+ * A string identifying the state of this attributes object. The string encodes the current values of all
+ * this object's properties. It's typically used to validate cached representations of shapes associated
+ * with this attributes object.
+ * @type {String}
+ * @readonly
+ * @memberof PlacemarkAttributes.prototype
+ */
+ stateKey: {
+ get: function () {
+ if (this.stateKeyInvalid) {
+ this._stateKey = this.computeStateKey();
+ this.stateKeyInvalid = false;
+ }
+ return this._stateKey;
+ }
+ },
+
+ /**
+ * The image color.
+ * When this attribute bundle has a valid image path the placemark's image is composed with this image
+ * color to achieve the final placemark color. Otherwise the placemark is drawn in this color. The color
+ * white, the default, causes the image to be drawn in its native colors.
+ * @type {Color}
+ * @default White (1, 1, 1, 1)
+ * @memberof PlacemarkAttributes.prototype
+ */
+ imageColor: {
+ get: function () {
+ return this._imageColor;
+ },
+ set: function (value) {
+ this._imageColor = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates the location within the placemark's image to align with the placemark's geographic position.
+ * May be null, in which case the image's bottom-left corner is placed at the geographic position.
+ * @type {Offset}
+ * @default 0.5, 0.5, both fractional (Centers the image on the geographic position.)
+ * @memberof PlacemarkAttributes.prototype
+ */
+ imageOffset: {
+ get: function () {
+ return this._imageOffset;
+ },
+ set: function (value) {
+ this._imageOffset = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates the amount to scale the placemark's image.
+ * When this attribute bundle has a valid image path the scale is applied to the image's dimensions. Otherwise the
+ * scale indicates the dimensions in pixels of a square drawn at the placemark's geographic position.
+ * A scale of 0 causes the placemark to disappear; however, the placemark's label, if any, is still drawn.
+ * @type {Number}
+ * @default 1
+ * @memberof PlacemarkAttributes.prototype
+ */
+ imageScale: {
+ get: function () {
+ return this._imageScale;
+ },
+ set: function (value) {
+ this._imageScale = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * The image source of the placemark's image. May be either a string giving the URL of the image, or an
+ * {@link ImageSource} object identifying an Image created dynamically.
+ * If null, the placemark is drawn as a square whose width and height are
+ * the value of this attribute object's [imageScale]{@link PlacemarkAttributes#imageScale} property.
+ * @type {String|ImageSource}
+ * @default null
+ * @memberof PlacemarkAttributes.prototype
+ */
+ imageSource: {
+ get: function () {
+ return this._imageSource;
+ },
+ set: function (value) {
+ this._imageSource = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates whether the placemark should be depth-tested against other objects in the scene. If true,
+ * the placemark may be occluded by terrain and other objects in certain viewing situations. If false,
+ * the placemark will not be occluded by terrain and other objects. If this value is true, the placemark's
+ * label, if any, has an independent depth-test control.
+ * See [PlacemarkAttributes.labelAttributes]{@link PlacemarkAttributes#labelAttributes}
+ * and [TextAttributes.depthTest]{@link TextAttributes#depthTest}.
+ * @type {Boolean}
+ * @default true
+ * @memberof PlacemarkAttributes.prototype
+ */
+ depthTest: {
+ get: function () {
+ return this._depthTest;
+ },
+ set: function (value) {
+ this._depthTest = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates the attributes to apply to the placemark's label, if any. If null, the placemark's label is
+ * not drawn.
+ * @type {TextAttributes}
+ * @default The defaults of {@link TextAttributes}.
+ * @memberof PlacemarkAttributes.prototype
+ */
+ labelAttributes: {
+ get: function () {
+ return this._labelAttributes;
+ },
+ set: function (value) {
+ this._labelAttributes = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates whether to draw a line from the placemark's geographic position to the ground.
+ * @type {Boolean}
+ * @default false
+ * @memberof PlacemarkAttributes.prototype
+ */
+ drawLeaderLine: {
+ get: function () {
+ return this._drawLeaderLine;
+ },
+ set: function (value) {
+ this._drawLeaderLine = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * The attributes to apply to the leader line if it's drawn. If null, the placemark's leader line is
+ * not drawn.
+ * @type {ShapeAttributes}
+ * @default The defaults of {@link ShapeAttributes}
+ * @memberof PlacemarkAttributes.prototype
+ */
+ leaderLineAttributes: {
+ get: function () {
+ return this._leaderLineAttributes;
+ },
+ set: function (value) {
+ this._leaderLineAttributes = value;
+ this.stateKeyInvalid = true;
+ }
+ }
+});
+
+export default PlacemarkAttributes;
diff --git a/web/test/WebWorldWind/src/shapes/Polygon.js b/web/test/WebWorldWind/src/shapes/Polygon.js
new file mode 100644
index 00000000..969828d4
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/Polygon.js
@@ -0,0 +1,902 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports Polygon
+ */
+import AbstractShape from '../shapes/AbstractShape';
+import ArgumentError from '../error/ArgumentError';
+import BasicTextureProgram from '../shaders/BasicTextureProgram';
+import BoundingBox from '../geom/BoundingBox';
+import Color from '../util/Color';
+import ImageSource from '../util/ImageSource';
+import Location from '../geom/Location';
+import Logger from '../util/Logger';
+import Matrix from '../geom/Matrix';
+import PickedObject from '../pick/PickedObject';
+import Position from '../geom/Position';
+import ShapeAttributes from '../shapes/ShapeAttributes';
+import SurfacePolygon from '../shapes/SurfacePolygon';
+import Vec2 from '../geom/Vec2';
+import Vec3 from '../geom/Vec3';
+import libtessDummy from '../util/libtess';
+
+
+/**
+ * Constructs a Polygon.
+ * @alias Polygon
+ * @constructor
+ * @augments AbstractShape
+ * @classdesc Represents a 3D polygon. The polygon may be extruded to the ground to form a prism. It may have
+ * multiple boundaries defining empty portions. See also {@link SurfacePolygon}.
+ *
+ *
+ * If the latter, the polygon positions' altitudes are ignored. (If the polygon should be draped onto the
+ * terrain, you might want to use {@link SurfacePolygon} instead.)
+ *
+ *
+ * @param {Location} center The circle's center location.
+ * @param {Number} radius The circle's radius in meters.
+ * @param {ShapeAttributes} attributes The attributes to apply to this shape. May be null, in which case
+ * attributes must be set directly before the shape is drawn.
+ * @throws {ArgumentError} If the specified center location is null or undefined or the specified radius
+ * is negative.
+ */
+function SurfaceCircle(center, radius, attributes) {
+ if (!center) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceCircle", "constructor", "missingLocation"));
+ }
+
+ if (radius < 0) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceCircle", "constructor", "Radius is negative"));
+ }
+
+ SurfaceShape.call(this, attributes);
+
+ // All these are documented with their property accessors below.
+ this._center = center;
+ this._radius = radius;
+ this._intervals = SurfaceCircle.DEFAULT_NUM_INTERVALS;
+}
+
+SurfaceCircle.prototype = Object.create(SurfaceShape.prototype);
+
+Object.defineProperties(SurfaceCircle.prototype, {
+ /**
+ * This shape's center location.
+ * @memberof SurfaceCircle.prototype
+ * @type {Location}
+ */
+ center: {
+ get: function () {
+ return this._center;
+ },
+ set: function (value) {
+ this.stateKeyInvalid = true;
+ this.resetBoundaries();
+ this._center = value;
+ }
+ },
+
+ /**
+ * This shape's radius, in meters.
+ * @memberof SurfaceCircle.prototype
+ * @type {Number}
+ */
+ radius: {
+ get: function () {
+ return this._radius;
+ },
+ set: function (value) {
+ this.stateKeyInvalid = true;
+ this.resetBoundaries();
+ this._radius = value;
+ }
+ },
+
+ /**
+ * The number of intervals to generate locations for.
+ * @type {Number}
+ * @memberof SurfaceCircle.prototype
+ * @default SurfaceCircle.DEFAULT_NUM_INTERVALS
+ */
+ intervals: {
+ get: function () {
+ return this._intervals;
+ },
+ set: function (value) {
+ this.stateKeyInvalid = true;
+ this.resetBoundaries();
+ this._intervals = value;
+ }
+ }
+});
+
+// Internal use only. Intentionally not documented.
+SurfaceCircle.staticStateKey = function (shape) {
+ var shapeStateKey = SurfaceShape.staticStateKey(shape);
+
+ return shapeStateKey +
+ " ce " + shape.center.toString() +
+ " ra " + shape.radius.toString();
+};
+
+// Internal use only. Intentionally not documented.
+SurfaceCircle.prototype.computeStateKey = function () {
+ return SurfaceCircle.staticStateKey(this);
+};
+
+// Internal. Intentionally not documented.
+SurfaceCircle.prototype.computeBoundaries = function (dc) {
+ if (this.radius === 0) {
+ return null;
+ }
+
+ var numLocations = 1 + Math.max(SurfaceCircle.MIN_NUM_INTERVALS, this.intervals),
+ da = 360 / (numLocations - 1),
+ arcLength = this.radius / dc.globe.radiusAt(this.center.latitude, this.center.longitude);
+
+ this._boundaries = new Array(numLocations);
+
+ for (var i = 0; i < numLocations; i++) {
+ var azimuth = i !== numLocations - 1 ? i * da : 0;
+ this._boundaries[i] = Location.greatCircleLocation(
+ this.center,
+ azimuth, // In degrees
+ arcLength, // In radians
+ new Location(0, 0)
+ );
+ }
+};
+
+// Internal use only. Intentionally not documented.
+SurfaceCircle.prototype.getReferencePosition = function () {
+ return this.center;
+};
+
+// Internal use only. Intentionally not documented.
+SurfaceCircle.prototype.moveTo = function (globe, position) {
+ this.center = position;
+};
+
+/**
+ * The minimum number of intervals the circle generates.
+ * @type {Number}
+ */
+SurfaceCircle.MIN_NUM_INTERVALS = 8;
+
+/**
+ * The default number of intervals the circle generates.
+ * @type {Number}
+ */
+SurfaceCircle.DEFAULT_NUM_INTERVALS = 64;
+
+export default SurfaceCircle;
+
diff --git a/web/test/WebWorldWind/src/shapes/SurfaceEllipse.js b/web/test/WebWorldWind/src/shapes/SurfaceEllipse.js
new file mode 100644
index 00000000..f760fd1f
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/SurfaceEllipse.js
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports SurfaceEllipse
+ */
+import Angle from '../geom/Angle';
+import ArgumentError from '../error/ArgumentError';
+import Location from '../geom/Location';
+import Logger from '../util/Logger';
+import ShapeAttributes from '../shapes/ShapeAttributes';
+import SurfaceShape from '../shapes/SurfaceShape';
+import WWMath from '../util/WWMath';
+
+
+/**
+ * Constructs a surface ellipse with a specified center and radii and an optional attributes bundle.
+ * @alias SurfaceEllipse
+ * @constructor
+ * @augments SurfaceShape
+ * @classdesc Represents an ellipse draped over the terrain surface.
+ *
+ *
+ * @param {Location} center The ellipse's center location.
+ * @param {Number} majorRadius The ellipse's major radius in meters.
+ * @param {Number} minorRadius The ellipse's minor radius in meters.
+ * @param {Number} heading The heading of the major axis in degrees.
+ * @param {ShapeAttributes} attributes The attributes to apply to this shape. May be null, in which case
+ * attributes must be set directly before the shape is drawn.
+ * @throws {ArgumentError} If the specified center location is null or undefined or if either specified radii
+ * is negative.
+ */
+function SurfaceEllipse(center, majorRadius, minorRadius, heading, attributes) {
+ if (!center) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceEllipse", "constructor", "missingLocation"));
+ }
+
+ if (majorRadius < 0 || minorRadius < 0) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceEllipse", "constructor", "Radius is negative."));
+ }
+
+ SurfaceShape.call(this, attributes);
+
+ // All these are documented with their property accessors below.
+ this._center = center;
+ this._majorRadius = majorRadius;
+ this._minorRadius = minorRadius;
+ this._heading = heading;
+ this._intervals = SurfaceEllipse.DEFAULT_NUM_INTERVALS;
+}
+
+SurfaceEllipse.prototype = Object.create(SurfaceShape.prototype);
+
+Object.defineProperties(SurfaceEllipse.prototype, {
+ /**
+ * This shape's center location.
+ * @memberof SurfaceEllipse.prototype
+ * @type {Location}
+ */
+ center: {
+ get: function () {
+ return this._center;
+ },
+ set: function (value) {
+ this.stateKeyInvalid = true;
+ this.resetBoundaries();
+ this._center = value;
+ }
+ },
+
+ /**
+ * This shape's major radius, in meters.
+ * @memberof SurfaceEllipse.prototype
+ * @type {Number}
+ */
+ majorRadius: {
+ get: function () {
+ return this._majorRadius;
+ },
+ set: function (value) {
+ this.stateKeyInvalid = true;
+ this.resetBoundaries();
+ this._majorRadius = value;
+ }
+ },
+
+ /**
+ * This shape's minor radius in meters.
+ * @memberof SurfaceEllipse.prototype
+ * @type {Number}
+ */
+ minorRadius: {
+ get: function () {
+ return this._minorRadius;
+ },
+ set: function (value) {
+ this.stateKeyInvalid = true;
+ this.resetBoundaries();
+ this._minorRadius = value;
+ }
+ },
+
+ /**
+ * The heading of the major axis, specified as degrees clockwise from North.
+ * @type {Number}
+ * @memberof SurfaceEllipse.prototype
+ * @default 0
+ */
+ heading: {
+ get: function () {
+ return this._heading;
+ },
+ set: function (value) {
+ this.stateKeyInvalid = true;
+ this.resetBoundaries();
+ this._heading = value;
+ }
+ },
+
+ /**
+ * The number of intervals to generate locations for.
+ * @type {Number}
+ * @memberof SurfaceEllipse.prototype
+ * @default SurfaceEllipse.DEFAULT_NUM_INTERVALS
+ */
+ intervals: {
+ get: function () {
+ return this._intervals;
+ },
+ set: function (value) {
+ this.stateKeyInvalid = true;
+ this.resetBoundaries();
+ this._intervals = value;
+ }
+ }
+});
+
+// Internal use only. Intentionally not documented.
+SurfaceEllipse.staticStateKey = function (shape) {
+ var shapeStateKey = SurfaceShape.staticStateKey(shape);
+
+ return shapeStateKey +
+ " ce " + shape.center.toString() +
+ " ma " + shape.majorRadius.toString() +
+ " mi " + shape.minorRadius.toString() +
+ " he " + shape.heading.toString() +
+ " in " + shape.intervals.toString();
+};
+
+// Internal use only. Intentionally not documented.
+SurfaceEllipse.prototype.computeStateKey = function () {
+ return SurfaceEllipse.staticStateKey(this);
+};
+
+// Internal. Intentionally not documented.
+SurfaceEllipse.prototype.computeBoundaries = function (dc) {
+ if (this.majorRadius == 0 && this.minorRadius == 0) {
+ return null;
+ }
+
+ var globe = dc.globe,
+ numLocations = 1 + Math.max(SurfaceEllipse.MIN_NUM_INTERVALS, this.intervals),
+ da = 2 * Math.PI / (numLocations - 1),
+ globeRadius = globe.radiusAt(this.center.latitude, this.center.longitude);
+
+ this._boundaries = new Array(numLocations);
+
+ for (var i = 0; i < numLocations; i++) {
+ var angle = i != numLocations - 1 ? i * da : 0,
+ xLength = this.majorRadius * Math.cos(angle),
+ yLength = this.minorRadius * Math.sin(angle),
+ distance = Math.sqrt(xLength * xLength + yLength * yLength);
+
+ // azimuth runs positive clockwise from north and through 360 degrees.
+ var azimuth = Math.PI / 2.0 -
+ (Math.acos(xLength / distance) * WWMath.signum(yLength) - this.heading * Angle.DEGREES_TO_RADIANS);
+
+ this._boundaries[i] = Location.greatCircleLocation(this.center, azimuth * Angle.RADIANS_TO_DEGREES,
+ distance / globeRadius, new Location(0, 0));
+ }
+};
+
+// Internal use only. Intentionally not documented.
+SurfaceEllipse.prototype.getReferencePosition = function () {
+ return this.center;
+};
+
+// Internal use only. Intentionally not documented.
+SurfaceEllipse.prototype.moveTo = function (globe, position) {
+ this.center = position;
+};
+
+/**
+ * The minimum number of intervals the ellipse generates.
+ * @type {Number}
+ */
+SurfaceEllipse.MIN_NUM_INTERVALS = 8;
+
+/**
+ * The default number of intervals the ellipse generates.
+ * @type {Number}
+ */
+SurfaceEllipse.DEFAULT_NUM_INTERVALS = 64;
+
+export default SurfaceEllipse;
+
diff --git a/web/test/WebWorldWind/src/shapes/SurfaceImage.js b/web/test/WebWorldWind/src/shapes/SurfaceImage.js
new file mode 100644
index 00000000..888d9324
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/SurfaceImage.js
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports SurfaceImage
+ */
+import ArgumentError from '../error/ArgumentError';
+import Logger from '../util/Logger';
+import PickedObject from '../pick/PickedObject';
+import SurfaceTile from '../render/SurfaceTile';
+
+
+/**
+ * Constructs a surface image shape for a specified sector and image path.
+ * @alias SurfaceImage
+ * @constructor
+ * @augments SurfaceTile
+ * @classdesc Represents an image drawn on the terrain.
+ * @param {Sector} sector The sector spanned by this surface image.
+ * @param {String|ImageSource} imageSource The image source of the image to draw on the terrain.
+ * May be either a string identifying the URL of the image, or an {@link ImageSource} object identifying a
+ * dynamically created image.
+ * @throws {ArgumentError} If either the specified sector or image source is null or undefined.
+ */
+function SurfaceImage(sector, imageSource) {
+ if (!sector) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceImage", "constructor",
+ "missingSector"));
+ }
+
+ if (!imageSource) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceImage", "constructor",
+ "missingImage"));
+ }
+
+ SurfaceTile.call(this, sector);
+
+ /**
+ * Indicates whether this surface image is drawn.
+ * @type {boolean}
+ * @default true
+ */
+ this.enabled = true;
+
+ /**
+ * The path to the image.
+ * @type {String}
+ */
+ this._imageSource = imageSource;
+
+ /**
+ * This surface image's resampling mode. Indicates the sampling algorithm used to display this image when it
+ * is larger on screen than its native resolution. May be one of:
+ *
+ *
+ * @default WorldWind.FILTER_LINEAR
+ */
+ this.resamplingMode = WorldWind.FILTER_LINEAR;
+
+ /**
+ * This surface image's opacity. When this surface image is drawn, the actual opacity is the product of
+ * this opacity and the opacity of the layer containing this surface image.
+ * @type {number}
+ */
+ this.opacity = 1;
+
+ /**
+ * This surface image's display name;
+ * @type {string}
+ */
+ this.displayName = "Surface Image";
+
+ // Internal. Indicates whether the image needs to be updated in the GPU resource cache.
+ this.imageSourceWasUpdated = true;
+}
+
+SurfaceImage.prototype = Object.create(SurfaceTile.prototype);
+
+Object.defineProperties(SurfaceImage.prototype, {
+ /**
+ * The source of the image to display.
+ * May be either a string identifying the URL of the image, or an {@link ImageSource} object identifying a
+ * dynamically created image.
+ * @type {String|ImageSource}
+ * @default null
+ * @memberof SurfaceImage.prototype
+ */
+ imageSource: {
+ get: function () {
+ return this._imageSource;
+ },
+ set: function (imageSource) {
+ if (!imageSource) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceImage", "imageSource",
+ "missingImage"));
+ }
+
+ this._imageSource = imageSource;
+ this.imageSourceWasUpdated = true;
+ }
+ }
+});
+
+SurfaceImage.prototype.bind = function (dc) {
+ var texture = dc.gpuResourceCache.resourceForKey(this._imageSource);
+ if (texture && !this.imageSourceWasUpdated) {
+ return this.bindTexture(dc, texture);
+ } else {
+ texture = dc.gpuResourceCache.retrieveTexture(dc.currentGlContext, this._imageSource);
+ this.imageSourceWasUpdated = false;
+ if (texture) {
+ return this.bindTexture(dc, texture);
+ }
+ }
+};
+
+SurfaceImage.prototype.bindTexture = function (dc, texture) {
+ var gl = dc.currentGlContext;
+
+ texture.setTexParameter(
+ gl.TEXTURE_MAG_FILTER,
+ this.resamplingMode === WorldWind.FILTER_NEAREST ? gl.NEAREST : gl.LINEAR
+ );
+
+ return texture.bind(dc);
+};
+
+SurfaceImage.prototype.applyInternalTransform = function (dc, matrix) {
+ // No need to apply the transform.
+};
+
+/**
+ * Displays this surface image. Called by the layer containing this surface image.
+ * @param {DrawContext} dc The current draw context.
+ */
+SurfaceImage.prototype.render = function (dc) {
+ if (!this.enabled) {
+ return;
+ }
+
+ if (!dc.terrain) {
+ return;
+ }
+
+ if (!this.sector.overlaps(dc.terrain.sector)) {
+ return;
+ }
+
+ if (dc.pickingMode) {
+ this.pickColor = dc.uniquePickColor();
+ }
+
+ dc.surfaceTileRenderer.renderTiles(dc, [this], this.opacity * dc.currentLayer.opacity);
+
+ if (dc.pickingMode) {
+ var po = new PickedObject(this.pickColor.clone(), this.pickDelegate ? this.pickDelegate : this,
+ null, this.layer, false);
+ dc.resolvePick(po);
+ }
+
+ dc.currentLayer.inCurrentFrame = true;
+};
+
+export default SurfaceImage;
diff --git a/web/test/WebWorldWind/src/shapes/SurfacePolygon.js b/web/test/WebWorldWind/src/shapes/SurfacePolygon.js
new file mode 100644
index 00000000..49382ad6
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/SurfacePolygon.js
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports SurfacePolygon
+ */
+import ArgumentError from '../error/ArgumentError';
+import Logger from '../util/Logger';
+import ShapeAttributes from '../shapes/ShapeAttributes';
+import SurfaceShape from '../shapes/SurfaceShape';
+
+
+/**
+ * Constructs a surface polygon.
+ * @alias SurfacePolygon
+ * @constructor
+ * @augments SurfaceShape
+ * @classdesc Represents a polygon draped over the terrain surface. The polygon may have multiple boundaries in
+ * order to define holes or empty regions.
+ *
+ *
+ * @param {Array} boundaries The polygons boundary locations. If this argument is an array of
+ * [Locations]{@link Location} they define this polygon's outer boundary. If it is an array of arrays of
+ * Locations then each array entry defines one of this polygon's boundaries.
+ * @param {ShapeAttributes} attributes The attributes to apply to this shape. May be null, in which case
+ * attributes must be set directly before the shape is drawn.
+ *
+ * @throws {ArgumentError} If the specified boundaries are null or undefined.
+ */
+function SurfacePolygon(boundaries, attributes) {
+ if (!Array.isArray(boundaries)) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "SurfacePolygon", "constructor",
+ "The specified boundary is not an array."));
+ }
+
+ SurfaceShape.call(this, attributes);
+
+ this._boundaries = boundaries;
+
+ this._stateId = SurfacePolygon.stateId++;
+}
+
+SurfacePolygon.prototype = Object.create(SurfaceShape.prototype);
+
+Object.defineProperties(SurfacePolygon.prototype, {
+ /**
+ * This polygon's boundaries. The polygons boundary locations. If this argument is an array of
+ * [Locations]{@link Location} they define this polygon's outer boundary. If it is an array of arrays of
+ * Locations then each array entry defines one of this polygon's boundaries.
+ * @type {Location[][] | Location[]}
+ * @memberof SurfacePolygon.prototype
+ */
+ boundaries: {
+ get: function () {
+ return this._boundaries;
+ },
+ set: function (boundaries) {
+ if (!Array.isArray(boundaries)) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "SurfacePolygon", "set boundaries",
+ "The specified value is not an array."));
+ }
+ this.resetBoundaries();
+ this._boundaries = boundaries;
+ this._stateId = SurfacePolygon.stateId++;
+ this.stateKeyInvalid = true;
+ }
+ }
+});
+
+// Internal use only. Intentionally not documented.
+SurfacePolygon.stateId = Number.MIN_SAFE_INTEGER;
+
+// Internal use only. Intentionally not documented.
+SurfacePolygon.staticStateKey = function (shape) {
+ var shapeStateKey = SurfaceShape.staticStateKey(shape);
+
+ return shapeStateKey +
+ " pg " + shape._stateId;
+};
+
+// Internal use only. Intentionally not documented.
+SurfacePolygon.prototype.computeStateKey = function () {
+ return SurfacePolygon.staticStateKey(this);
+};
+
+// Internal. Polygon doesn't generate its own boundaries. See SurfaceShape.prototype.computeBoundaries.
+SurfacePolygon.prototype.computeBoundaries = function (dc) {
+};
+
+// Internal use only. Intentionally not documented.
+SurfacePolygon.prototype.getReferencePosition = function () {
+ // Assign the first position as the reference position.
+ if (this.boundaries.length > 0 && this.boundaries[0].length > 2) {
+ return this.boundaries[0][0];
+ } else if (this.boundaries.length > 2) {
+ return this.boundaries[0];
+ } else {
+ return null;
+ }
+};
+
+// Internal use only. Intentionally not documented.
+SurfacePolygon.prototype.moveTo = function (globe, position) {
+ if (this.boundaries.length > 0 && this.boundaries[0].length > 2) {
+ var boundaries = [];
+ for (var i = 0, len = this._boundaries.length; i < len; i++) {
+ var locations = this.computeShiftedLocations(globe, this.getReferencePosition(), position,
+ this._boundaries[i]);
+ boundaries.push(locations);
+ }
+ this.boundaries = boundaries;
+ } else if (this.boundaries.length > 2) {
+ this.boundaries = this.computeShiftedLocations(globe, this.getReferencePosition(), position,
+ this._boundaries);
+ }
+};
+
+export default SurfacePolygon;
+
diff --git a/web/test/WebWorldWind/src/shapes/SurfacePolyline.js b/web/test/WebWorldWind/src/shapes/SurfacePolyline.js
new file mode 100644
index 00000000..3718bd92
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/SurfacePolyline.js
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports SurfacePolyline
+ */
+import ArgumentError from '../error/ArgumentError';
+import Logger from '../util/Logger';
+import ShapeAttributes from '../shapes/ShapeAttributes';
+import SurfaceShape from '../shapes/SurfaceShape';
+
+
+/**
+ * Constructs a surface polyline.
+ * @alias SurfacePolyline
+ * @constructor
+ * @augments SurfaceShape
+ * @classdesc Represents a polyline draped over the terrain surface.
+ *
+ *
+ * @param {Location[]} locations This polyline's locations.
+ * @param {ShapeAttributes} attributes The attributes to apply to this shape. May be null, in which case
+ * attributes must be set directly before the shape is drawn.
+ * @throws {ArgumentError} If the specified locations are null or undefined.
+ */
+function SurfacePolyline(locations, attributes) {
+ if (!locations) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "SurfacePolyline", "constructor",
+ "The specified locations array is null or undefined."));
+ }
+
+ SurfaceShape.call(this, attributes);
+
+ /**
+ * This shape's locations, specified as an array locations.
+ * @type {Array}
+ */
+ this._boundaries = locations;
+
+ this._stateId = SurfacePolyline.stateId++;
+
+ // Internal use only.
+ this._isInteriorInhibited = true;
+}
+
+SurfacePolyline.prototype = Object.create(SurfaceShape.prototype);
+
+Object.defineProperties(SurfacePolyline.prototype, {
+ /**
+ * This polyline's boundaries. The polylines locations.
+ * @type {Location[]}
+ * @memberof SurfacePolyline.prototype
+ */
+ boundaries: {
+ get: function () {
+ return this._boundaries;
+ },
+ set: function (boundaries) {
+ if (!Array.isArray(boundaries)) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "SurfacePolyline", "set boundaries",
+ "The specified value is not an array."));
+ }
+ this.resetBoundaries();
+ this._boundaries = boundaries;
+ this._stateId = SurfacePolyline.stateId++;
+ this.stateKeyInvalid = true;
+ }
+ }
+});
+
+// Internal use only. Intentionally not documented.
+SurfacePolyline.stateId = Number.MIN_SAFE_INTEGER;
+
+// Internal use only. Intentionally not documented.
+SurfacePolyline.staticStateKey = function (shape) {
+ var shapeStateKey = SurfaceShape.staticStateKey(shape);
+
+ return shapeStateKey +
+ " pl " + shape._stateId;
+};
+
+// Internal use only. Intentionally not documented.
+SurfacePolyline.prototype.computeStateKey = function () {
+ return SurfacePolyline.staticStateKey(this);
+};
+
+// Internal. Polyline doesn't generate its own boundaries. See SurfaceShape.prototype.computeBoundaries.
+SurfacePolyline.prototype.computeBoundaries = function (dc) {
+};
+
+// Internal use only. Intentionally not documented.
+SurfacePolyline.prototype.getReferencePosition = function () {
+ return this.boundaries.length > 1 ? this.boundaries[0] : null;
+};
+
+// Internal use only. Intentionally not documented.
+SurfacePolyline.prototype.moveTo = function (globe, position) {
+ this.boundaries = this.computeShiftedLocations(globe, this.getReferencePosition(), position,
+ this._boundaries);
+};
+
+export default SurfacePolyline;
+
diff --git a/web/test/WebWorldWind/src/shapes/SurfaceRectangle.js b/web/test/WebWorldWind/src/shapes/SurfaceRectangle.js
new file mode 100644
index 00000000..17c2fd29
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/SurfaceRectangle.js
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports SurfaceRectangle
+ */
+import Angle from '../geom/Angle';
+import ArgumentError from '../error/ArgumentError';
+import Location from '../geom/Location';
+import Logger from '../util/Logger';
+import ShapeAttributes from '../shapes/ShapeAttributes';
+import SurfaceShape from '../shapes/SurfaceShape';
+import WWMath from '../util/WWMath';
+
+
+/**
+ * Constructs a surface rectangle with a specified center and size and an optional attributes bundle.
+ * @alias SurfaceRectangle
+ * @constructor
+ * @augments SurfaceShape
+ * @classdesc Represents a rectangle draped over the terrain surface.
+ *
+ *
+ * @param {Location} center The rectangle's center location.
+ * @param {Number} width The rectangle's width in meters.
+ * @param {Number} height The rectangle's height in meters.
+ * @param {Number} heading The rectangle's heading.
+ * @param {ShapeAttributes} attributes The attributes to apply to this shape. May be null, in which case
+ * attributes must be set directly before the shape is drawn.
+ * @throws {ArgumentError} If the specified center location is null or undefined or if either specified width
+ * or height is negative.
+ */
+function SurfaceRectangle(center, width, height, heading, attributes) {
+ if (!center) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceRectangle", "constructor", "missingLocation"));
+ }
+
+ if (width < 0 || height < 0) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceRectangle", "constructor", "Size is negative."));
+ }
+
+ SurfaceShape.call(this, attributes);
+
+ // All these are documented with their property accessors below.
+ this._center = center;
+ this._width = width;
+ this._height = height;
+ this._heading = heading;
+}
+
+SurfaceRectangle.prototype = Object.create(SurfaceShape.prototype);
+
+Object.defineProperties(SurfaceRectangle.prototype, {
+ /**
+ * This shape's center location.
+ * @memberof SurfaceRectangle.prototype
+ * @type {Location}
+ */
+ center: {
+ get: function () {
+ return this._center;
+ },
+ set: function (value) {
+ this.stateKeyInvalid = true;
+ this.resetBoundaries();
+ this._center = value;
+ }
+ },
+
+ /**
+ * This shape's width, in meters.
+ * @memberof SurfaceRectangle.prototype
+ * @type {Number}
+ */
+ width: {
+ get: function () {
+ return this._width;
+ },
+ set: function (value) {
+ this.stateKeyInvalid = true;
+ this.resetBoundaries();
+ this._width = value;
+ }
+ },
+
+ /**
+ * This shape's height in meters.
+ * @memberof SurfaceRectangle.prototype
+ * @type {Number}
+ */
+ height: {
+ get: function () {
+ return this._height;
+ },
+ set: function (value) {
+ this.stateKeyInvalid = true;
+ this.resetBoundaries();
+ this._height = value;
+ }
+ },
+
+ /**
+ * The shape's heading, specified as degrees clockwise from North. This shape's height and width are
+ * relative to its heading.
+ * @memberof SurfaceRectangle.prototype
+ * @type {number}
+ * @default 0
+ */
+ heading: {
+ get: function () {
+ return this._heading;
+ },
+ set: function (value) {
+ this.stateKeyInvalid = true;
+ this.resetBoundaries();
+ this._heading = value;
+ }
+ }
+});
+
+// Internal use only. Intentionally not documented.
+SurfaceRectangle.staticStateKey = function (shape) {
+ var shapeStateKey = SurfaceShape.staticStateKey(shape);
+
+ return shapeStateKey +
+ " ce " + shape.center.toString() +
+ " wi " + shape.width.toString() +
+ " he " + shape.height.toString() +
+ " hd " + shape.heading.toString();
+};
+
+// Internal use only. Intentionally not documented.
+SurfaceRectangle.prototype.computeStateKey = function () {
+ return SurfaceRectangle.staticStateKey(this);
+};
+
+// Internal. Intentionally not documented.
+SurfaceRectangle.prototype.computeBoundaries = function (dc) {
+ var halfWidth = 0.5 * this.width,
+ halfHeight = 0.5 * this.height,
+ globeRadius = dc.globe.radiusAt(this.center.latitude, this.center.longitude);
+
+
+ this._boundaries = new Array(4);
+
+ this.addLocation(0, -halfWidth, -halfHeight, globeRadius);
+ this.addLocation(1, halfWidth, -halfHeight, globeRadius);
+ this.addLocation(2, halfWidth, halfHeight, globeRadius);
+ this.addLocation(3, -halfWidth, halfHeight, globeRadius);
+};
+
+SurfaceRectangle.prototype.addLocation = function (idx, xLength, yLength, globeRadius) {
+ var distance = Math.sqrt(xLength * xLength + yLength * yLength);
+
+ // azimuth runs positive clockwise from north and through 360 degrees.
+ var azimuth = Math.PI / 2.0 - (Math.acos(xLength / distance) * WWMath.signum(yLength) - this.heading * Angle.DEGREES_TO_RADIANS);
+
+ this._boundaries[idx] = Location.greatCircleLocation(this.center, azimuth * Angle.RADIANS_TO_DEGREES,
+ distance / globeRadius, new Location(0, 0));
+
+};
+
+// Internal use only. Intentionally not documented.
+SurfaceRectangle.prototype.getReferencePosition = function () {
+ return this.center;
+};
+
+// Internal use only. Intentionally not documented.
+SurfaceRectangle.prototype.moveTo = function (globe, position) {
+ this.center = this.computeShiftedLocations(globe, this.getReferencePosition(), position, [this.center])[0];
+};
+
+export default SurfaceRectangle;
+
diff --git a/web/test/WebWorldWind/src/shapes/SurfaceSector.js b/web/test/WebWorldWind/src/shapes/SurfaceSector.js
new file mode 100644
index 00000000..55de63f7
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/SurfaceSector.js
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports SurfaceSector
+ */
+import ArgumentError from '../error/ArgumentError';
+import Location from '../geom/Location';
+import Logger from '../util/Logger';
+import ShapeAttributes from '../shapes/ShapeAttributes';
+import SurfaceShape from '../shapes/SurfaceShape';
+
+
+/**
+ * Constructs a surface sector.
+ * @alias SurfaceSector
+ * @constructor
+ * @augments SurfaceShape
+ * @classdesc Represents a sector draped over the terrain surface. The sector is specified as a rectangular
+ * region in geographic coordinates. By default, a surface sector is drawn with a linear path, see
+ * {@link SurfaceShape#pathType}.
+ *
+ *
+ * @param {Sector} sector This surface sector's sector.
+ * @param {ShapeAttributes} attributes The attributes to apply to this shape. May be null, in which case
+ * attributes must be set directly before the shape is drawn.
+ * @throws {ArgumentError} If the specified boundaries are null or undefined.
+ */
+function SurfaceSector(sector, attributes) {
+ if (!sector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceSector", "constructor", "missingSector"));
+ }
+
+ SurfaceShape.call(this, attributes);
+
+ /**
+ * This shape's sector.
+ * @type {Sector}
+ */
+ this._sector = sector;
+
+ // The default path type for a surface sector is linear so that it represents a bounding box by default.
+ this._pathType = WorldWind.LINEAR;
+}
+
+SurfaceSector.prototype = Object.create(SurfaceShape.prototype);
+
+Object.defineProperties(SurfaceSector.prototype, {
+ /**
+ * This shape's sector.
+ * @memberof SurfaceSector.prototype
+ * @type {Sector}
+ */
+ sector: {
+ get: function () {
+ return this._sector;
+ },
+ set: function (value) {
+ this.stateKeyInvalid = true;
+ this.resetBoundaries();
+ this._sector = value;
+ }
+ }
+});
+
+// Internal use only. Intentionally not documented.
+SurfaceSector.staticStateKey = function (shape) {
+ var shapeStateKey = SurfaceShape.staticStateKey(shape);
+
+ return shapeStateKey;
+};
+
+// Internal use only. Intentionally not documented.
+SurfaceSector.prototype.computeStateKey = function () {
+ return SurfaceSector.staticStateKey(this);
+};
+
+// Internal. Intentionally not documented.
+SurfaceSector.prototype.computeBoundaries = function (dc) {
+ var sector = this._sector;
+
+ this._boundaries = new Array(4);
+
+ this._boundaries[0] = new Location(sector.minLatitude, sector.minLongitude);
+ this._boundaries[1] = new Location(sector.maxLatitude, sector.minLongitude);
+ this._boundaries[2] = new Location(sector.maxLatitude, sector.maxLongitude);
+ this._boundaries[3] = new Location(sector.minLatitude, sector.maxLongitude);
+};
+
+// Internal use only. Intentionally not documented.
+SurfaceSector.prototype.getReferencePosition = function () {
+ return new Location(this.sector.centroidLatitude(), this.sector.centroidLongitude());
+};
+
+// Internal use only. Intentionally not documented.
+SurfaceSector.prototype.moveTo = function (globe, position) {
+ var sector = this._sector;
+
+ var locations = new Array(3);
+
+ locations[0] = new Location(sector.minLatitude, sector.minLongitude);
+ locations[1] = new Location(sector.maxLatitude, sector.minLongitude);
+ locations[2] = new Location(sector.maxLatitude, sector.maxLongitude);
+
+ locations = this.computeShiftedLocations(globe, this.getReferencePosition(), position, locations);
+
+ this.sector = new WorldWind.Sector(
+ locations[0].latitude,
+ locations[1].latitude,
+ locations[1].longitude,
+ locations[2].longitude
+ );
+};
+
+export default SurfaceSector;
+
diff --git a/web/test/WebWorldWind/src/shapes/SurfaceShape.js b/web/test/WebWorldWind/src/shapes/SurfaceShape.js
new file mode 100644
index 00000000..66fe22da
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/SurfaceShape.js
@@ -0,0 +1,1063 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports SurfaceShape
+ */
+import AbstractError from '../error/AbstractError';
+import Angle from '../geom/Angle';
+import ArgumentError from '../error/ArgumentError';
+import BoundingBox from '../geom/BoundingBox';
+import Color from '../util/Color';
+import Location from '../geom/Location';
+import Logger from '../util/Logger';
+import MemoryCache from '../cache/MemoryCache';
+import NotYetImplementedError from '../error/NotYetImplementedError';
+import PickedObject from '../pick/PickedObject';
+import PolygonSplitter from '../util/PolygonSplitter';
+import Renderable from '../render/Renderable';
+import Sector from '../geom/Sector';
+import ShapeAttributes from '../shapes/ShapeAttributes';
+import UnsupportedOperationError from '../error/UnsupportedOperationError';
+import Vec3 from '../geom/Vec3';
+
+
+/**
+ * Constructs a surface shape with an optionally specified bundle of default attributes.
+ * @alias SurfaceShape
+ * @constructor
+ * @augments Renderable
+ * @abstract
+ * @classdesc Represents a surface shape. This is an abstract base class and is meant to be instantiated
+ * only by subclasses.
+ *
+ *
+ * @memberof SurfaceShape.prototype
+ * @type {String}
+ * @default WorldWind.GREAT_CIRCLE
+ */
+ pathType: {
+ get: function () {
+ return this._pathType;
+ },
+ set: function (value) {
+ this.stateKeyInvalid = true;
+ this.resetBoundaries();
+ this._pathType = value;
+ }
+ },
+
+ /**
+ * The maximum number of intervals an edge will be broken into. This is the number of intervals that an
+ * edge that spans to opposite side of the globe would be broken into. This is strictly an upper bound
+ * and the number of edge intervals may be lower if this resolution is not needed.
+ * @memberof SurfaceShape.prototype
+ * @type {Number}
+ * @default SurfaceShape.DEFAULT_NUM_EDGE_INTERVALS
+ */
+ maximumNumEdgeIntervals: {
+ get: function () {
+ return this._maximumNumEdgeIntervals;
+ },
+ set: function (value) {
+ this.stateKeyInvalid = true;
+ this.resetBoundaries();
+ this._maximumNumEdgeIntervals = value;
+ }
+ },
+
+ /**
+ * A dimensionless number that controls throttling of edge traversal near the poles where edges need to be
+ * sampled more closely together.
+ * A value of 0 indicates that no polar throttling is to be performed.
+ * @memberof SurfaceShape.prototype
+ * @type {Number}
+ * @default SurfaceShape.DEFAULT_POLAR_THROTTLE
+ */
+ polarThrottle: {
+ get: function () {
+ return this._polarThrottle;
+ },
+ set: function (value) {
+ this.stateKeyInvalid = true;
+ this.resetBoundaries();
+ this._polarThrottle = value;
+ }
+ },
+
+ /**
+ * Defines the extent of the shape in latitude and longitude.
+ * This sector only has valid data once the boundary is defined. Prior to this, it is null.
+ * @memberof SurfaceShape.prototype
+ * @type {Sector}
+ */
+ boundingSector: {
+ get: function () {
+ return this._boundingSector;
+ }
+ }
+});
+
+SurfaceShape.staticStateKey = function (shape) {
+ shape.stateKeyInvalid = false;
+
+ if (shape.highlighted) {
+ if (!shape._highlightAttributes) {
+ if (!shape._attributes) {
+ shape._attributesStateKey = null;
+ } else {
+ shape._attributesStateKey = shape._attributes.stateKey;
+ }
+ } else {
+ shape._attributesStateKey = shape._highlightAttributes.stateKey;
+ }
+ } else {
+ if (!shape._attributes) {
+ shape._attributesStateKey = null;
+ } else {
+ shape._attributesStateKey = shape._attributes.stateKey;
+ }
+ }
+
+ return "dn " + shape.displayName +
+ " at " + (!shape._attributesStateKey ? "null" : shape._attributesStateKey) +
+ " hi " + shape.highlighted +
+ " en " + shape.enabled +
+ " pt " + shape.pathType +
+ " ne " + shape.maximumNumEdgeIntervals +
+ " po " + shape.polarThrottle +
+ " se " + "[" +
+ shape.boundingSector.minLatitude + "," +
+ shape.boundingSector.maxLatitude + "," +
+ shape.boundingSector.minLongitude + "," +
+ shape.boundingSector.maxLongitude +
+ "]";
+};
+
+SurfaceShape.prototype.computeStateKey = function () {
+ return SurfaceShape.staticStateKey(this);
+};
+
+/**
+ * Returns this shape's area in square meters.
+ * @param {Globe} globe The globe on which to compute the area.
+ * @param {Boolean} terrainConformant If true, the returned area is that of the terrain,
+ * including its hillsides and other undulations. If false, the returned area is the shape's
+ * projected area.
+ */
+SurfaceShape.prototype.area = function (globe, terrainConformant) {
+ throw new NotYetImplementedError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceShape", "area", "notYetImplemented"));
+};
+
+// Internal function. Intentionally not documented.
+SurfaceShape.prototype.computeBoundaries = function (globe) {
+ // This method is in the base class and should be overridden if the boundaries are generated.
+ // TODO: Incorrect error class
+ throw new AbstractError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceShape", "computeBoundaries", "abstractInvocation"));
+};
+
+// Internal. Intentionally not documented.
+SurfaceShape.prototype.intersectsFrustum = function (dc) {
+ if (this.currentData && this.currentData.extent) {
+ if (dc.pickingMode) {
+ return this.currentData.extent.intersectsFrustum(dc.pickFrustum);
+ } else {
+ return this.currentData.extent.intersectsFrustum(dc.frustumInModelCoordinates);
+ }
+ } else {
+ return true;
+ }
+};
+
+/**
+ * Indicates whether a specified shape data object is current. Subclasses may override this method to add
+ * criteria indicating whether the shape data object is current, but must also call this method on this base
+ * class. Applications do not call this method.
+ * @param {DrawContext} dc The current draw context.
+ * @param {Object} shapeData The object to validate.
+ * @returns {Boolean} true if the object is current, otherwise false.
+ * @protected
+ */
+SurfaceShape.prototype.isShapeDataCurrent = function (dc, shapeData) {
+ return shapeData.verticalExaggeration === dc.verticalExaggeration
+ && shapeData.expiryTime > Date.now();
+};
+
+/**
+ * Creates a new shape data object for the current globe state. Subclasses may override this method to
+ * modify the shape data object that this method creates, but must also call this method on this base class.
+ * Applications do not call this method.
+ * @returns {Object} The shape data object.
+ * @protected
+ */
+SurfaceShape.prototype.createShapeDataObject = function () {
+ return {};
+};
+
+// Intentionally not documented.
+SurfaceShape.prototype.resetExpiration = function (shapeData) {
+ // The random addition in the line below prevents all shapes from regenerating during the same frame.
+ shapeData.expiryTime = Date.now() + this.expirationInterval + 1e3 * Math.random();
+};
+
+// Internal. Intentionally not documented.
+SurfaceShape.prototype.establishCurrentData = function (dc) {
+ this.currentData = this.shapeDataCache.entryForKey(dc.globeStateKey);
+ if (!this.currentData) {
+ this.currentData = this.createShapeDataObject();
+ this.resetExpiration(this.currentData);
+ this.shapeDataCache.putEntry(dc.globeStateKey, this.currentData, 1);
+ }
+
+ this.currentData.isExpired = !this.isShapeDataCurrent(dc, this.currentData);
+};
+
+// Internal function. Intentionally not documented.
+SurfaceShape.prototype.render = function (dc) {
+ if (!this.enabled) {
+ return;
+ }
+
+ this.layer = dc.currentLayer;
+
+ this.prepareBoundaries(dc);
+
+ this.establishCurrentData(dc);
+
+ if (this.currentData.isExpired || !this.currentData.extent) {
+ this.computeExtent(dc);
+ this.currentData.verticalExaggeration = dc.verticalExaggeration;
+ this.resetExpiration(this.currentData);
+ }
+
+ // Use the last computed extent to see if this shape is out of view.
+ if (this.currentData && this.currentData.extent && !this.intersectsFrustum(dc)) {
+ return;
+ }
+
+ dc.surfaceShapeTileBuilder.insertSurfaceShape(this);
+};
+
+// Internal function. Intentionally not documented.
+SurfaceShape.prototype.interpolateLocations = function (locations) {
+ var first = locations[0],
+ next = first,
+ prev,
+ isNextFirst = true,
+ isPrevFirst = true,// Don't care initially, this will get set in first iteration.
+ countFirst = 0,
+ isInterpolated = true,
+ idx, len;
+
+ this._locations = [first];
+
+ for (idx = 1, len = locations.length; idx < len; idx += 1) {
+ // Advance to next location, retaining previous location.
+ prev = next;
+ isPrevFirst = isNextFirst;
+
+ next = locations[idx];
+
+ // Detect whether the next location and the first location are the same.
+ isNextFirst = next.latitude == first.latitude && next.longitude == first.longitude;
+
+ // Inhibit interpolation if either endpoint if the first location,
+ // except for the first segement which will be the actual first location or that location
+ // as the polygon closes the first time.
+ // All subsequent encounters of the first location are used to connected secondary domains with the
+ // primary domain in multiply-connected geometry (an outer ring with multiple inner rings).
+ isInterpolated = true;
+ if (isNextFirst || isPrevFirst) {
+ countFirst += 1;
+
+ if (countFirst > 2) {
+ isInterpolated = false;
+ }
+ }
+
+ if (isInterpolated) {
+ this.interpolateEdge(prev, next, this._locations);
+ }
+
+ this._locations.push(next);
+
+ prev = next;
+ }
+
+ // Force the closing of the border.
+ if (!this._isInteriorInhibited) {
+ // Avoid duplication if the first endpoint was already emitted.
+ if (prev.latitude != first.latitude || prev.longitude != first.longitude) {
+ this.interpolateEdge(prev, first, this._locations);
+ this._locations.push(first);
+ }
+ }
+};
+
+// Internal function. Intentionally not documented.
+SurfaceShape.prototype.interpolateEdge = function (start, end, locations) {
+ var distanceRadians = Location.greatCircleDistance(start, end),
+ steps = Math.round(this._maximumNumEdgeIntervals * distanceRadians / Math.PI),
+ dt,
+ location;
+
+ if (steps > 0) {
+ dt = 1 / steps;
+ location = start;
+
+ for (var t = this.throttledStep(dt, location); t < 1; t += this.throttledStep(dt, location)) {
+ location = new Location(0, 0);
+ Location.interpolateAlongPath(this._pathType, t, start, end, location);
+
+ //florin: ensure correct longitude sign and decimal error for anti-meridian
+ if (start.longitude === 180 && end.longitude === 180) {
+ location.longitude = 180;
+ }
+ else if (start.longitude === -180 && end.longitude === -180) {
+ location.longitude = -180;
+ }
+
+ locations.push(location);
+ }
+ }
+};
+
+// Internal function. Intentionally not documented.
+// Return a throttled step size when near the poles.
+SurfaceShape.prototype.throttledStep = function (dt, location) {
+ var cosLat = Math.cos(location.latitude * Angle.DEGREES_TO_RADIANS);
+ cosLat *= cosLat; // Square cos to emphasize poles and de-emphasize equator.
+
+ // Remap polarThrottle:
+ // 0 .. INF => 0 .. 1
+ // This acts as a weight between no throttle and fill throttle.
+ var weight = this._polarThrottle / (1 + this._polarThrottle);
+
+ return dt * (1 - weight + weight * cosLat);
+};
+
+// Internal function. Intentionally not documented.
+SurfaceShape.prototype.prepareBoundaries = function (dc) {
+ if (this.boundariesArePrepared) {
+ return;
+ }
+
+ this.computeBoundaries(dc);
+
+ var newBoundaries = this.formatBoundaries();
+ this.normalizeAngles(newBoundaries);
+ newBoundaries = this.interpolateBoundaries(newBoundaries);
+
+ var contoursInfo = [];
+ var doesCross = PolygonSplitter.splitContours(newBoundaries, contoursInfo);
+ this.contours = contoursInfo;
+ this.crossesAntiMeridian = doesCross;
+
+ this.prepareGeometry(dc, contoursInfo);
+
+ this.prepareSectors();
+
+ this.boundariesArePrepared = true;
+};
+
+//Internal. Formats the boundaries of a surface shape to be a multi dimensional array
+SurfaceShape.prototype.formatBoundaries = function () {
+ var boundaries = [];
+ if (!this._boundaries.length) {
+ return boundaries;
+ }
+ if (this._boundaries[0].latitude != null) {
+ //not multi dim array
+ boundaries.push(this._boundaries);
+ }
+ else {
+ boundaries = this._boundaries;
+ }
+ return boundaries;
+};
+
+// Internal. Resets boundaries for SurfaceShape recomputing.
+SurfaceShape.prototype.resetBoundaries = function () {
+ this.boundariesArePrepared = false;
+ this.shapeDataCache.clear(false);
+};
+
+// Internal use only. Intentionally not documented.
+SurfaceShape.prototype.normalizeAngles = function (boundaries) {
+ for (var i = 0, len = boundaries.length; i < len; i++) {
+ var polygon = boundaries[i];
+ for (var j = 0, lenP = polygon.length; j < lenP; j++) {
+ var point = polygon[j];
+ if (point.longitude < -180 || point.longitude > 180) {
+ point.longitude = Angle.normalizedDegreesLongitude(point.longitude);
+ }
+ if (point.latitude < -90 || point.latitude > 90) {
+ point.latitude = Angle.normalizedDegreesLatitude(point.latitude);
+ }
+ }
+ }
+};
+
+// Internal use only. Intentionally not documented.
+SurfaceShape.prototype.interpolateBoundaries = function (boundaries) {
+ var newBoundaries = [];
+ for (var i = 0, len = boundaries.length; i < len; i++) {
+ var contour = boundaries[i];
+ this.interpolateLocations(contour);
+ newBoundaries.push(this._locations.slice());
+ this._locations.length = 0;
+ }
+ return newBoundaries;
+};
+
+/**
+ * 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 {DrawContext} dc The drawing context containing a globe.
+ *
+ * @return {Sector[]} Bounding sectors for the shape.
+ */
+SurfaceShape.prototype.computeSectors = function (dc) {
+ // Return a previously computed value if it already exists.
+ if (this._boundingSectors && this._boundingSectors.length > 0) {
+ return this._boundingSectors;
+ }
+
+ this.prepareBoundaries(dc);
+
+ return this._boundingSectors;
+};
+
+/**
+ * Computes the extent for the shape based on its sectors.
+ *
+ * @param {DrawContext} dc The drawing context containing a globe.
+ *
+ * @return {BoundingBox} The extent for the shape.
+ */
+SurfaceShape.prototype.computeExtent = function (dc) {
+
+ if (!this._boundingSectors || this._boundingSectors.length === 0) {
+ return null;
+ }
+
+ if (!this.currentData) {
+ return null;
+ }
+
+ if (!this.currentData.extent) {
+ this.currentData.extent = new BoundingBox();
+ }
+
+
+ var boxPoints;
+ // This surface shape does not cross the international dateline, and therefore has a single bounding sector.
+ // Return the box which contains that sector.
+ if (this._boundingSectors.length === 1) {
+ boxPoints = this._boundingSectors[0].computeBoundingPoints(dc.globe, dc.verticalExaggeration);
+ this.currentData.extent.setToVec3Points(boxPoints);
+ }
+ // 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 {
+ var boxCorners = [];
+
+ for (var i = 0; i < this._boundingSectors.length; i++) {
+ boxPoints = this._boundingSectors[i].computeBoundingPoints(dc.globe, dc.verticalExaggeration);
+ var box = new BoundingBox();
+ box.setToVec3Points(boxPoints);
+ var corners = box.getCorners();
+ for (var j = 0; j < corners.length; j++) {
+ boxCorners.push(corners[j]);
+ }
+ }
+ this.currentData.extent.setToVec3Points(boxCorners);
+ }
+
+ return this.currentData.extent;
+
+};
+
+/**
+ * Computes a new set of locations translated from a specified location to a new location for a shape.
+ *
+ * @param {Globe} globe The globe on which to compute a new set of locations.
+ * @param {Location} oldLocation The original reference location.
+ * @param {Location} newLocation The new reference location.
+ * @param {Location[]} locations The locations to translate.
+ *
+ * @return {Location[]} The translated locations.
+ */
+SurfaceShape.prototype.computeShiftedLocations = function (globe, oldLocation, newLocation, locations) {
+ var newLocations = [];
+ var result = new Vec3(0, 0, 0);
+ var newPos = new WorldWind.Position(0, 0, 0);
+
+ var oldPoint = globe.computePointFromLocation(oldLocation.latitude, oldLocation.longitude,
+ new Vec3(0, 0, 0));
+ var newPoint = globe.computePointFromLocation(newLocation.latitude, newLocation.longitude,
+ new Vec3(0, 0, 0));
+ var delta = newPoint.subtract(oldPoint);
+
+ for (var i = 0, len = locations.length; i < len; i++) {
+ globe.computePointFromLocation(locations[i].latitude, locations[i].longitude, result);
+ result.add(delta);
+ globe.computePositionFromPoint(result[0], result[1], result[2], newPos);
+ newLocations.push(new Location(newPos.latitude, newPos.longitude));
+ }
+
+ return newLocations;
+};
+
+// Internal use only. Intentionally not documented.
+SurfaceShape.prototype.prepareSectors = function () {
+ this.determineSectors();
+ if (this.crossesAntiMeridian) {
+ this.sectorsOverAntiMeridian();
+ }
+ else {
+ this.sectorsNotOverAntiMeridian();
+ }
+};
+
+// Internal use only. Intentionally not documented.
+SurfaceShape.prototype.determineSectors = function () {
+ for (var i = 0, len = this.contours.length; i < len; i++) {
+ var contour = this.contours[i];
+ var polygons = contour.polygons;
+ contour.sectors = [];
+ for (var j = 0, lenP = polygons.length; j < lenP; j++) {
+ var polygon = polygons[j];
+ var sector = new Sector(0, 0, 0, 0);
+ sector.setToBoundingSector(polygon);
+ if (this._pathType === WorldWind.GREAT_CIRCLE) {
+ var extremes = Location.greatCircleArcExtremeLocations(polygon);
+ var minLatitude = Math.min(sector.minLatitude, extremes[0].latitude);
+ var maxLatitude = Math.max(sector.maxLatitude, extremes[1].latitude);
+ sector.minLatitude = minLatitude;
+ sector.maxLatitude = maxLatitude;
+ }
+ contour.sectors.push(sector);
+ }
+ }
+};
+
+// Internal use only. Intentionally not documented.
+SurfaceShape.prototype.sectorsOverAntiMeridian = function () {
+ var eastSector = new Sector(90, -90, 180, -180); //positive
+ var westSector = new Sector(90, -90, 180, -180); //negative
+ for (var i = 0, len = this.contours.length; i < len; i++) {
+ var sectors = this.contours[i].sectors;
+ for (var j = 0, lenS = sectors.length; j < lenS; j++) {
+ var sector = sectors[j];
+ if (sector.minLongitude < 0 && sector.maxLongitude > 0) {
+ westSector.union(sector);
+ eastSector.union(sector);
+ }
+ else if (sector.minLongitude < 0) {
+ westSector.union(sector);
+ }
+ else {
+ eastSector.union(sector);
+ }
+ }
+ }
+ var minLatitude = Math.min(eastSector.minLatitude, westSector.minLatitude);
+ var maxLatitude = Math.max(eastSector.maxLatitude, eastSector.maxLatitude);
+ this._boundingSector = new Sector(minLatitude, maxLatitude, -180, 180);
+ this._boundingSectors = [eastSector, westSector];
+};
+
+// Internal use only. Intentionally not documented.
+SurfaceShape.prototype.sectorsNotOverAntiMeridian = function () {
+ this._boundingSector = new Sector(90, -90, 180, -180);
+ for (var i = 0, len = this.contours.length; i < len; i++) {
+ var sectors = this.contours[i].sectors;
+ for (var j = 0, lenS = sectors.length; j < lenS; j++) {
+ this._boundingSector.union(sectors[j]);
+ }
+ }
+ this._boundingSectors = [this._boundingSector];
+};
+
+// Internal use only. Intentionally not documented.
+SurfaceShape.prototype.prepareGeometry = function (dc, contours) {
+ var interiorPolygons = [];
+ var outlinePolygons = [];
+
+ for (var i = 0, len = contours.length; i < len; i++) {
+ var contour = contours[i];
+ var poleIndex = contour.poleIndex;
+
+ for (var j = 0, lenC = contour.polygons.length; j < lenC; j++) {
+ var polygon = contour.polygons[j];
+ var iMap = contour.iMap[j];
+ interiorPolygons.push(polygon);
+
+ if (contour.pole !== Location.poles.NONE && lenC > 1) {
+ //split with pole
+ if (j === poleIndex) {
+ this.outlineForPole(polygon, iMap, outlinePolygons);
+ }
+ else {
+ this.outlineForSplit(polygon, iMap, outlinePolygons);
+ }
+ }
+ else if (contour.pole !== Location.poles.NONE && lenC === 1) {
+ //only pole
+ this.outlineForPole(polygon, iMap, outlinePolygons);
+ }
+ else if (contour.pole === Location.poles.NONE && lenC > 1) {
+ //only split
+ this.outlineForSplit(polygon, iMap, outlinePolygons);
+ }
+ else if (contour.pole === Location.poles.NONE && lenC === 1) {
+ //no pole, no split
+ outlinePolygons.push(polygon);
+ }
+ }
+ }
+
+ this._interiorGeometry = interiorPolygons;
+ this._outlineGeometry = outlinePolygons;
+};
+
+// Internal use only. Intentionally not documented.
+SurfaceShape.prototype.outlineForPole = function (polygon, iMap, outlinePolygons) {
+ this.containsPole = true;
+ var outlinePolygon = [];
+ var pCount = 0;
+ for (var k = 0, lenP = polygon.length; k < lenP; k++) {
+ var point = polygon[k];
+ var intersection = iMap.get(k);
+ if (intersection && intersection.forPole) {
+ pCount++;
+ if (pCount % 2 === 1) {
+ outlinePolygon.push(point);
+ outlinePolygons.push(outlinePolygon);
+ outlinePolygon = [];
+ }
+ }
+ if (pCount % 2 === 0) {
+ outlinePolygon.push(point);
+ }
+ }
+ if (outlinePolygon.length) {
+ outlinePolygons.push(outlinePolygon);
+ }
+};
+
+// Internal use only. Intentionally not documented.
+SurfaceShape.prototype.outlineForSplit = function (polygon, iMap, outlinePolygons) {
+ var outlinePolygon = [];
+ var iCount = 0;
+ for (var k = 0, lenP = polygon.length; k < lenP; k++) {
+ var point = polygon[k];
+ var intersection = iMap.get(k);
+ if (intersection && !intersection.forPole) {
+ iCount++;
+ if (iCount % 2 === 0) {
+ outlinePolygon.push(point);
+ outlinePolygons.push(outlinePolygon);
+ outlinePolygon = [];
+ }
+ }
+ if (iCount % 2 === 1) {
+ outlinePolygon.push(point);
+ }
+ }
+};
+
+// Internal use only. Intentionally not documented.
+SurfaceShape.prototype.resetPickColor = function () {
+ this.pickColor = null;
+};
+
+/**
+ * Internal use only.
+ * Render the shape onto the texture map of the tile.
+ * @param {DrawContext} dc The draw context to render onto.
+ * @param {CanvasRenderingContext2D} ctx2D The rendering context for SVG.
+ * @param {Number} xScale The multiplicative scale factor in the horizontal direction.
+ * @param {Number} yScale The multiplicative scale factor in the vertical direction.
+ * @param {Number} dx The additive offset in the horizontal direction.
+ * @param {Number} dy The additive offset in the vertical direction.
+ */
+SurfaceShape.prototype.renderToTexture = function (dc, ctx2D, xScale, yScale, dx, dy) {
+ var attributes = this._highlighted ? this._highlightAttributes || this._attributes : this._attributes;
+ if (!attributes) {
+ return;
+ }
+
+ var drawInterior = !this._isInteriorInhibited && attributes.drawInterior;
+ var drawOutline = attributes.drawOutline && attributes.outlineWidth > 0;
+ if (!drawInterior && !drawOutline) {
+ return;
+ }
+
+ if (dc.pickingMode) {
+ if (!this.pickColor) {
+ this.pickColor = dc.uniquePickColor();
+ }
+ ctx2D.fillStyle = this.pickColor.toCssColorString();
+ ctx2D.strokeStyle = ctx2D.fillStyle;
+ ctx2D.lineWidth = attributes.outlineWidth;
+ } else {
+ var ic = attributes.interiorColor,
+ oc = attributes.outlineColor;
+ ctx2D.fillStyle = new Color(ic.red, ic.green, ic.blue, ic.alpha * this.layer.opacity).toCssColorString();
+ ctx2D.strokeStyle = new Color(oc.red, oc.green, oc.blue, oc.alpha * this.layer.opacity).toCssColorString();
+ ctx2D.lineWidth = attributes.outlineWidth;
+ }
+
+ if (this.crossesAntiMeridian || this.containsPole) {
+ if (drawInterior) {
+ this.draw(this._interiorGeometry, ctx2D, xScale, yScale, dx, dy);
+ ctx2D.fill();
+ }
+ if (drawOutline) {
+ this.draw(this._outlineGeometry, ctx2D, xScale, yScale, dx, dy);
+ ctx2D.stroke();
+ }
+ }
+ else {
+ this.draw(this._interiorGeometry, ctx2D, xScale, yScale, dx, dy);
+ if (drawInterior) {
+ ctx2D.fill();
+ }
+ if (drawOutline) {
+ ctx2D.stroke();
+ }
+ }
+
+ if (dc.pickingMode) {
+ var po = new PickedObject(this.pickColor.clone(), this.pickDelegate ? this.pickDelegate : this,
+ null, this.layer, false);
+ dc.resolvePick(po);
+ }
+};
+
+SurfaceShape.prototype.draw = function (contours, ctx2D, xScale, yScale, dx, dy) {
+ ctx2D.beginPath();
+ for (var i = 0, len = contours.length; i < len; i++) {
+ var contour = contours[i];
+ var point = contour[0];
+ var x = point.longitude * xScale + dx;
+ var y = point.latitude * yScale + dy;
+ ctx2D.moveTo(x, y);
+ for (var j = 1, lenC = contour.length; j < lenC; j++) {
+ point = contour[j];
+ x = point.longitude * xScale + dx;
+ y = point.latitude * yScale + dy;
+ ctx2D.lineTo(x, y);
+ }
+ }
+};
+
+/**
+ * Default value for the maximum number of edge intervals. This results in a maximum error of 480 m for an arc
+ * that spans the entire globe.
+ *
+ * Other values for this parameter have the associated errors below:
+ * Intervals Maximum error (meters)
+ * 2 1280253.5
+ * 4 448124.5
+ * 8 120837.6
+ * 16 30628.3
+ * 32 7677.9
+ * 64 1920.6
+ * 128 480.2
+ * 256 120.0
+ * 512 30.0
+ * 1024 7.5
+ * 2048 1.8
+ * The errors cited above are upper bounds and the actual error may be lower.
+ * @type {Number}
+ */
+SurfaceShape.DEFAULT_NUM_EDGE_INTERVALS = 128;
+
+/**
+ * The defualt value for the polar throttle, which slows edge traversal near the poles.
+ * @type {Number}
+ */
+SurfaceShape.DEFAULT_POLAR_THROTTLE = 10;
+
+export default SurfaceShape;
diff --git a/web/test/WebWorldWind/src/shapes/SurfaceShapeTile.js b/web/test/WebWorldWind/src/shapes/SurfaceShapeTile.js
new file mode 100644
index 00000000..99f0b66d
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/SurfaceShapeTile.js
@@ -0,0 +1,248 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports SurfaceShapeTile
+ */
+import Angle from '../geom/Angle';
+import ArgumentError from '../error/ArgumentError';
+import Level from '../util/Level';
+import Logger from '../util/Logger';
+import Sector from '../geom/Sector';
+import Texture from '../render/Texture';
+import TextureTile from '../render/TextureTile';
+
+
+/**
+ * Constructs a surface shape tile.
+ * @alias SurfaceShapeTile
+ * @constructor
+ * @classdesc Represents a texture map containing renditions of surface shapes applied to a portion of a globe's terrain.
+ * @param {Sector} sector The sector this tile covers.
+ * @param {Level} level The level this tile is associated with.
+ * @param {number} row This tile's row in the associated level.
+ * @param {number} column This tile's column in the associated level.
+ * @throws {ArgumentError} If the specified sector or level is null or undefined, the row or column arguments
+ * are less than zero, or the specified image path is null, undefined or empty.
+ *
+ */
+function SurfaceShapeTile(sector, level, row, column) {
+ TextureTile.call(this, sector, level, row, column); // args are checked in the superclass' constructor
+
+ /**
+ * The surface shapes that affect this tile.
+ * @type {SurfaceShape[]}
+ */
+ this.surfaceShapes = [];
+
+ // Internal use only. Intentionally not documented.
+ this.surfaceShapeStateKeys = [];
+
+ // Internal use only. Intentionally not documented.
+ this.asRenderedSurfaceShapeStateKeys = [];
+
+ /**
+ * The sector that bounds this tile.
+ * @type {Sector}
+ */
+ this.sector = sector;
+
+ /**
+ * A string to use as a cache key.
+ * @type {string}
+ */
+ this.cacheKey = null;
+
+ // Internal use only. Intentionally not documented.
+ this.pickSequence = 0;
+
+ this.createCtx2D();
+}
+
+SurfaceShapeTile.prototype = Object.create(TextureTile.prototype);
+
+/**
+ * Clear all collected surface shapes.
+ */
+SurfaceShapeTile.prototype.clearShapes = function () {
+ // Clear out next surface shape.
+ this.surfaceShapes = [];
+ this.surfaceShapeStateKeys = [];
+};
+
+/**
+ * Query whether any surface shapes have been collected.
+ * @returns {boolean} Returns true if there are collected surface shapes.
+ */
+SurfaceShapeTile.prototype.hasShapes = function () {
+ return this.surfaceShapes.length > 0;
+};
+
+/**
+ * Get all shapes that this tile references.
+ * @returns {SurfaceShape[]} The collection of surface shapes referenced by this tile.
+ */
+SurfaceShapeTile.prototype.getShapes = function () {
+ return this.surfaceShapes;
+};
+
+/**
+ * Set the shapes this tile should reference.
+ * @param {SurfaceShape[]} surfaceShapes The collection of surface shapes to be referenced by this tile.
+ */
+SurfaceShapeTile.prototype.setShapes = function (surfaceShapes) {
+ this.surfaceShapes = surfaceShapes;
+};
+
+/**
+ * The sector that bounds this tile.
+ * @returns {Sector}
+ */
+SurfaceShapeTile.prototype.getSector = function () {
+ return this.sector;
+};
+
+/**
+ * Add a surface shape to this tile's collection of surface shapes.
+ * @param {SurfaceShape} surfaceShape The surface shape to add.
+ */
+SurfaceShapeTile.prototype.addSurfaceShape = function (surfaceShape) {
+ this.surfaceShapes.push(surfaceShape);
+ this.surfaceShapeStateKeys.push(surfaceShape.stateKey + " lo " + surfaceShape.layer.opacity); // combine the shape state key with layer opacity
+};
+
+// Internal use only. Intentionally not documented.
+SurfaceShapeTile.prototype.needsUpdate = function (dc) {
+ var idx, len, surfaceShape, surfaceShapeStateKey;
+
+ // If the number of surface shapes does not match the number of surface shapes already in the texture
+ if (this.surfaceShapes.length != this.asRenderedSurfaceShapeStateKeys.length) {
+ return true;
+ }
+
+ // If the state key of the shape is different from the saved state key (in order or configuration)
+ for (idx = 0, len = this.surfaceShapes.length; idx < len; idx += 1) {
+ if (this.surfaceShapeStateKeys[idx] !== this.asRenderedSurfaceShapeStateKeys[idx]) {
+ return true;
+ }
+ }
+
+ // If a texture does not already exist, ...
+ if (!this.hasTexture(dc)) {
+ return true;
+ }
+
+ // If you get here, the texture can be reused.
+ return false;
+};
+
+/**
+ * Determine whether the surface shape tile has a valid texture.
+ * @param {DrawContext} dc The draw context.
+ * @returns {boolean} True if the surface shape tile has a valid texture, else false.
+ */
+SurfaceShapeTile.prototype.hasTexture = function (dc) {
+ if (dc.pickingMode) {
+ return false;
+ }
+
+ if (!this.gpuCacheKey) {
+ this.gpuCacheKey = this.getCacheKey();
+ }
+
+ return dc.gpuResourceCache.containsResource(this.gpuCacheKey);
+};
+
+/**
+ * Redraw all of the surface shapes onto the texture for this tile.
+ * @param {DrawContext} dc
+ * @returns {Texture}
+ */
+SurfaceShapeTile.prototype.updateTexture = function (dc) {
+ var gl = dc.currentGlContext,
+ canvas = SurfaceShapeTile.canvas,
+ ctx2D = SurfaceShapeTile.ctx2D;
+
+ canvas.width = this.tileWidth;
+ canvas.height = this.tileHeight;
+
+ // Mapping from lat/lon to x/y:
+ // lon = minlon => x = 0
+ // lon = maxLon => x = 256
+ // lat = minLat => y = 256
+ // lat = maxLat => y = 0
+ // (assuming texture size is 256)
+ // So:
+ // x = 256 / sector.dlon * (lon - minLon)
+ // y = -256 / sector.dlat * (lat - maxLat)
+ var xScale = this.tileWidth / this.sector.deltaLongitude(),
+ yScale = -this.tileHeight / this.sector.deltaLatitude(),
+ xOffset = -this.sector.minLongitude * xScale,
+ yOffset = -this.sector.maxLatitude * yScale;
+
+ // Reset the surface shape state keys
+ this.asRenderedSurfaceShapeStateKeys = [];
+
+ for (var idx = 0, len = this.surfaceShapes.length; idx < len; idx += 1) {
+ var shape = this.surfaceShapes[idx];
+ this.asRenderedSurfaceShapeStateKeys.push(this.surfaceShapeStateKeys[idx]);
+
+ shape.renderToTexture(dc, ctx2D, xScale, yScale, xOffset, yOffset);
+ }
+
+ this.gpuCacheKey = this.getCacheKey();
+
+ var gpuResourceCache = dc.gpuResourceCache;
+ var texture = new Texture(gl, canvas);
+ gpuResourceCache.putResource(this.gpuCacheKey, texture, texture.size);
+ gpuResourceCache.setResourceAgingFactor(this.gpuCacheKey, 10); // age this texture 10x faster than normal resources (e.g., tiles)
+
+ return texture;
+};
+
+/**
+ * Get a key suitable for cache look-ups.
+ * @returns {string}
+ */
+SurfaceShapeTile.prototype.getCacheKey = function () {
+ if (!this.cacheKey) {
+ this.cacheKey = "SurfaceShapeTile:" +
+ this.tileKey + "," +
+ this.pickSequence.toString();
+ }
+
+ return this.cacheKey;
+};
+
+/**
+ * Create a new canvas and its 2D context on demand.
+ */
+SurfaceShapeTile.prototype.createCtx2D = function () {
+ // If the context was previously created, ...
+ if (!SurfaceShapeTile.ctx2D) {
+ SurfaceShapeTile.canvas = document.createElement("canvas");
+ SurfaceShapeTile.ctx2D = SurfaceShapeTile.canvas.getContext("2d");
+ }
+};
+
+/*
+ * For internal use only.
+ * 2D canvas and context, which is created lazily on demand.
+ */
+SurfaceShapeTile.canvas = null;
+SurfaceShapeTile.ctx2D = null;
+
+export default SurfaceShapeTile;
diff --git a/web/test/WebWorldWind/src/shapes/SurfaceShapeTileBuilder.js b/web/test/WebWorldWind/src/shapes/SurfaceShapeTileBuilder.js
new file mode 100644
index 00000000..20081fb9
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/SurfaceShapeTileBuilder.js
@@ -0,0 +1,512 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports SurfaceShapeTileBuilder
+ */
+import ArgumentError from '../error/ArgumentError';
+import DrawContext from '../render/DrawContext';
+import Globe from '../globe/Globe';
+import GpuProgram from '../shaders/GpuProgram';
+import Level from '../util/Level';
+import LevelSet from '../util/LevelSet';
+import Location from '../geom/Location';
+import Logger from '../util/Logger';
+import Matrix from '../geom/Matrix';
+import MemoryCache from '../cache/MemoryCache';
+import NotYetImplementedError from '../error/NotYetImplementedError';
+import PickedObject from '../pick/PickedObject';
+import Rectangle from '../geom/Rectangle';
+import Sector from '../geom/Sector';
+import SurfaceShape from '../shapes/SurfaceShape';
+import SurfaceShapeTile from '../shapes/SurfaceShapeTile';
+import Terrain from '../globe/Terrain';
+import TerrainTile from '../globe/TerrainTile';
+import TerrainTileList from '../globe/TerrainTileList';
+import TextureTile from '../render/TextureTile';
+import Tile from '../util/Tile';
+import WWMath from '../util/WWMath';
+
+
+function SurfaceShapeTileBuilder() {
+ // Parameterize top level subdivision in one place.
+
+ // TilesInTopLevel describes the most coarse tile structure.
+ this.numRowsTilesInTopLevel = 1;
+ this.numColumnsTilesInTopLevel = 1;
+
+ // The maximum number of levels that will ever be tessellated.
+ this.maximumSubdivisionDepth = 18;
+
+ // tileWidth, tileHeight - the number of subdivisions a single tile has; this determines the sampling grid.
+ this.tileWidth = 256;
+ this.tileHeight = 256;
+
+ /**
+ * The collection of levels.
+ * @type {LevelSet}
+ */
+ this.levels = new LevelSet(
+ Sector.FULL_SPHERE,
+ new Location(
+ 360 / this.numRowsTilesInTopLevel,
+ 360 / this.numColumnsTilesInTopLevel),
+ this.maximumSubdivisionDepth,
+ this.tileWidth,
+ this.tileHeight);
+
+ /**
+ * The collection of surface shapes processed by this class.
+ * @type {SurfaceShape[]}
+ */
+ this.surfaceShapes = [];
+
+ /**
+ * The collection of surface shape tiles that actually contain surface shapes.
+ * @type {SurfaceShapeTile[]}
+ */
+ this.surfaceShapeTiles = [];
+
+ /**
+ * The collection of top level surface shape tiles, from which actual tiles are derived.
+ * @type {SurfaceShapeTile[]}
+ */
+ this.topLevelTiles = [];
+
+ /**
+ * Accumulator of all sectors for surface shapes
+ * @type {Sector}
+ */
+ this.sector = new Sector(-WWMath.MAX_LAT, WWMath.MAX_LAT, -180, 180);
+
+ /**
+ * 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.
+ *
+ * @type {Number}
+ */
+ this.detailControl = 1.25;
+
+ // Internal use only. Intentionally not documented.
+ this.tileCache = new MemoryCache(500000, 400000);
+}
+
+/**
+ * Clear all transient state from the surface shape tile builder.
+ */
+SurfaceShapeTileBuilder.prototype.clear = function () {
+ this.surfaceShapeTiles.splice(0, this.surfaceShapeTiles.length);
+ this.surfaceShapes.splice(0, this.surfaceShapes.length);
+};
+
+/**
+ * Insert a surface shape to be rendered into the surface shape tile builder.
+ *
+ * @param {SurfaceShape} surfaceShape A surfave shape to be processed.
+ */
+SurfaceShapeTileBuilder.prototype.insertSurfaceShape = function (surfaceShape) {
+ this.surfaceShapes.push(surfaceShape);
+};
+
+/**
+ * Perform the rendering of any accumulated surface shapes by building the surface shape tiles that contain these
+ * shapes and then rendering those tiles.
+ *
+ * @param {DrawContext} dc The drawing context.
+ */
+SurfaceShapeTileBuilder.prototype.doRender = function (dc) {
+ if (dc.pickingMode) {
+ // Picking rendering strategy:
+ // 1) save all tiles created prior to picking,
+ // 2) construct and render new tiles with pick-based contents (colored with pick IDs),
+ // 3) restore all prior tiles.
+ // This has a big potential win for normal rendering, since there is a lot of coherence
+ // from frame to frame if no picking is occurring.
+ for (var idx = 0, len = this.surfaceShapes.length; idx < len; idx += 1) {
+ this.surfaceShapes[idx].resetPickColor();
+ }
+
+ SurfaceShapeTileBuilder.pickSequence += 1;
+
+ var savedTiles = this.surfaceShapeTiles;
+ var savedTopLevelTiles = this.topLevelTiles;
+
+ this.surfaceShapeTiles = [];
+ this.topLevelTiles = [];
+
+ this.buildTiles(dc);
+
+ if (dc.deepPicking) {
+ // Normally, we render all shapes together in one tile (or a small number, but this detail
+ // doesn't matter). For deep picking, we need to render each shape individually.
+ this.doDeepPickingRender(dc);
+
+ } else {
+ dc.surfaceTileRenderer.renderTiles(dc, this.surfaceShapeTiles, 1);
+ }
+
+ this.surfaceShapeTiles = savedTiles;
+ this.topLevelTiles = savedTopLevelTiles;
+ } else {
+ this.buildTiles(dc);
+
+ dc.surfaceTileRenderer.renderTiles(dc, this.surfaceShapeTiles, 1);
+ }
+};
+
+SurfaceShapeTileBuilder.prototype.doDeepPickingRender = function (dc) {
+ var idxTile, lenTiles, idxShape, lenShapes, idxPick, lenPicks, po, shape, tile;
+
+ // Determine the shapes that were drawn during buildTiles. These shapes may not actually be
+ // at the pick point, but they are candidates for deep picking.
+ var deepPickShapes = [];
+ for (idxPick = 0, lenPicks = dc.objectsAtPickPoint.objects.length; idxPick < lenPicks; idxPick += 1) {
+ po = dc.objectsAtPickPoint.objects[idxPick];
+ if (po.userObject instanceof SurfaceShape) {
+ shape = po.userObject;
+
+ // If the shape was not already in the collection of deep picked shapes, ...
+ if (deepPickShapes.indexOf(shape) < 0) {
+ deepPickShapes.push(shape);
+
+ // Delete the shape that was drawn during buildTiles from the pick list.
+ dc.objectsAtPickPoint.objects.splice(idxPick, 1);
+
+ // Update the index and length to reflect the deletion.
+ idxPick -= 1;
+ lenPicks -= 1;
+ }
+ }
+ }
+
+ if (deepPickShapes.length <= 0) {
+ return;
+ }
+
+ // For all shapes,
+ // 1) force that shape to be the only shape in a tile,
+ // 2) re-render the tile, and
+ // 3) use the surfaceTileRenderer to render the tile on the terrain,
+ // 4) read the color to see if it is attributable to the current shape.
+ var resolvablePickObjects = [];
+ for (idxShape = 0, lenShapes = deepPickShapes.length; idxShape < lenShapes; idxShape += 1) {
+ shape = deepPickShapes[idxShape];
+ for (idxTile = 0, lenTiles = this.surfaceShapeTiles.length; idxTile < lenTiles; idxTile += 1) {
+ tile = this.surfaceShapeTiles[idxTile];
+ tile.setShapes([shape]);
+ tile.updateTexture(dc);
+ }
+
+ dc.surfaceTileRenderer.renderTiles(dc, this.surfaceShapeTiles, 1);
+
+ var pickColor = dc.readPickColor(dc.pickPoint);
+ if (!!pickColor && shape.pickColor.equals(pickColor)) {
+ po = new PickedObject(shape.pickColor.clone(),
+ shape.pickDelegate ? shape.pickDelegate : shape, null, shape.layer, false);
+ resolvablePickObjects.push(po);
+ }
+ }
+
+ // Flush surface shapes that have accumulated in the updateTexture pass just completed on all shapes.
+ for (idxPick = 0, lenPicks = dc.objectsAtPickPoint.objects.length; idxPick < lenPicks; idxPick += 1) {
+ po = dc.objectsAtPickPoint.objects[idxPick];
+ if (po.userObject instanceof SurfaceShape) {
+ // Delete the shape that was picked in the most recent pass.
+ dc.objectsAtPickPoint.objects.splice(idxPick, 1);
+
+ // Update the index and length to reflect the deletion.
+ idxPick -= 1;
+ lenPicks -= 1;
+ }
+ }
+
+ // Add the resolvable pick objects for surface shapes that were actually visible at the pick point
+ // to the pick list.
+ for (idxPick = 0, lenPicks = resolvablePickObjects.length; idxPick < lenPicks; idxPick += 1) {
+ po = resolvablePickObjects[idxPick];
+ dc.objectsAtPickPoint.objects.push(po);
+ }
+};
+
+/**
+ * Assembles the surface tiles and draws any surface shapes that have been accumulated into those offscreen tiles. The
+ * surface tiles are assembled to meet the necessary resolution of to the draw context's.
+ *
+ * This does nothing if there are no surface shapes associated with this builder.
+ *
+ * @param {DrawContext} dc The draw context to build tiles for.
+ *
+ * @throws {ArgumentError} If the draw context is null.
+ */
+SurfaceShapeTileBuilder.prototype.buildTiles = function (dc) {
+ if (!dc) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceShapeTileBuilder", "buildTiles", "missingDc"));
+ }
+
+ if (!this.surfaceShapes || this.surfaceShapes.length < 1) {
+ return;
+ }
+
+ // Assemble the current visible tiles and update their associated textures if necessary.
+ this.assembleTiles(dc);
+
+ // Clean up references to all surface shapes to avoid dangling references. The surface shape list is no
+ // longer needed, now that the shapes are held by each tile.
+ this.surfaceShapes.splice(0, this.surfaceShapes.length);
+ for (var idx = 0, len = this.surfaceShapeTiles.length; idx < len; idx += 1) {
+ var tile = this.surfaceShapeTiles[idx];
+ tile.clearShapes();
+ }
+};
+
+/**
+ * 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 surface shape 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.
+ *
+ * During assembly, each surface shape in {@link #surfaceShapes} is sorted into the tiles they
+ * intersect. The top level tiles are used as an index to quickly determine which tiles each shape intersects.
+ * Surface shapes are sorted into sub-tiles by simple intersection tests, and are added to each tile's surface
+ * renderable list at most once. See {@link SurfaceShapeTileBuilder.SurfaceShapeTile#addSurfaceShape(SurfaceShape,
+ * gov.nasa.worldwind.geom.Sector)}. Tiles that don't intersect any surface shapes are discarded.
+ *
+ * @param {DrawContext} dc The DrawContext to assemble tiles for.
+ */
+SurfaceShapeTileBuilder.prototype.assembleTiles = function (dc) {
+ var tile, idxShape, lenShapes, idxTile, lenTiles, idxSector, lenSectors;
+
+ // Create a set of top level tiles only if that set doesn't exist yet.
+ if (this.topLevelTiles.length < 1) {
+ this.createTopLevelTiles();
+ }
+
+ // 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 shape in a set to ensure that each object is added to a tile at most once.
+ var intersectingTiles = {};
+
+ // Iterate over the current surface shapes, adding each surface shape to the top level tiles that it
+ // intersects. This produces a set of top level tiles containing the surface shapes that intersect each
+ // tile. We use the tile structure as an index to quickly determine the tiles a surface shape 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 (idxShape = 0, lenShapes = this.surfaceShapes.length; idxShape < lenShapes; idxShape += 1) {
+ var surfaceShape = this.surfaceShapes[idxShape];
+
+ var sectors = surfaceShape.computeSectors(dc);
+ if (!sectors) {
+ continue;
+ }
+
+ for (idxSector = 0, lenSectors = sectors.length; idxSector < lenSectors; idxSector += 1) {
+ var sector = sectors[idxSector];
+
+ for (idxTile = 0, lenTiles = this.topLevelTiles.length; idxTile < lenTiles; idxTile += 1) {
+ tile = this.topLevelTiles[idxTile];
+
+ if (tile.sector.intersects(sector)) {
+ var cacheKey = tile.tileKey;
+ intersectingTiles[cacheKey] = tile;
+ tile.addSurfaceShape(surfaceShape);
+ }
+ }
+ }
+ }
+
+ // Add each top level tile or its descendants to the current tile list.
+ //for (var idxTile = 0, lenTiles = this.topLevelTiles.length; idxTile < lenTiles; idxTile += 1) {
+ for (var key in intersectingTiles) {
+ if (intersectingTiles.hasOwnProperty(key)) {
+ tile = intersectingTiles[key];
+
+ this.addTileOrDescendants(dc, this.levels, null, tile);
+ }
+ }
+};
+
+/**
+ * Potentially adds the specified tile or its descendants to the tile builder's surface shape tile collection.
+ * The tile and its descendants are discarded if the tile is not visible or does not intersect any surface shapes in the
+ * parent's surface shape list.
+ *
+ * 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.
+ *
+ * @param {DrawContext} dc The current DrawContext.
+ * @param {LevelSet} levels The tile's LevelSet.
+ * @param {SurfaceShapeTile} parentTile The tile's parent, or null if the tile is a top level tile.
+ * @param {SurfaceShapeTile} tile The tile to add.
+ */
+SurfaceShapeTileBuilder.prototype.addTileOrDescendants = function (dc, levels, parentTile, tile) {
+ // Ignore this tile if it falls completely outside the frustum. This may be the viewing frustum or the pick
+ // frustum, depending on the implementation.
+ if (!this.intersectsFrustum(dc, tile)) {
+ // 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.clearShapes();
+ return;
+ }
+
+ // If the parent tile is not null, add any parent surface shapes that intersect this tile.
+ if (parentTile != null) {
+ this.addIntersectingShapes(dc, parentTile, tile);
+ }
+
+ // Ignore tiles that do not intersect any surface shapes.
+ if (!tile.hasShapes()) {
+ return;
+ }
+
+ // 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, levels, tile)) {
+ this.addTile(dc, tile);
+ return;
+ }
+
+ var nextLevel = levels.level(tile.level.levelNumber + 1);
+ var subTiles = dc.pickingMode ?
+ tile.subdivide(nextLevel, this) :
+ tile.subdivideToCache(nextLevel, this, this.tileCache);
+ for (var idxTile = 0, lenTiles = subTiles.length; idxTile < lenTiles; idxTile += 1) {
+ var subTile = subTiles[idxTile];
+ this.addTileOrDescendants(dc, levels, tile, 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.clearShapes();
+};
+
+/**
+ * Adds surface shapes from the parent's object list to the specified tile's object list. Adds any of the
+ * parent's surface shapes that intersect the tile's sector to the tile's object list.
+ *
+ * @param {DrawContext} dc The current DrawContext.
+ * @param {SurfaceShapeTile} parentTile The tile's parent.
+ * @param {SurfaceShapeTile} tile The tile to add intersecting surface shapes to.
+ */
+SurfaceShapeTileBuilder.prototype.addIntersectingShapes = function (dc, parentTile, tile) {
+ var shapes = parentTile.getShapes();
+ for (var idxShape = 0, lenShapes = shapes.length; idxShape < lenShapes; idxShape += 1) {
+ var shape = shapes[idxShape];
+
+ var sectors = shape.computeSectors(dc);
+ if (!sectors) {
+ continue;
+ }
+
+ // Test intersection against each of the surface shape's sectors. We break after finding an
+ // intersection to avoid adding the same object to the tile more than once.
+ for (var idxSector = 0, lenSectors = sectors.length; idxSector < lenSectors; idxSector += 1) {
+ var sector = sectors[idxSector];
+
+ if (tile.getSector().intersects(sector)) {
+ tile.addSurfaceShape(shape);
+ break;
+ }
+ }
+ }
+};
+
+/**
+ * Adds the specified tile to this tile builder's surface tile collection.
+ *
+ * @param {DrawContext} dc The draw context.
+ * @param {SurfaceShapeTile} tile The tile to add.
+ */
+SurfaceShapeTileBuilder.prototype.addTile = function (dc, tile) {
+ if (dc.pickingMode) {
+ tile.pickSequence = SurfaceShapeTileBuilder.pickSequence;
+ }
+
+ if (tile.needsUpdate(dc)) {
+ tile.updateTexture(dc);
+ }
+
+ this.surfaceShapeTiles.push(tile);
+};
+
+/**
+ * Internal use only.
+ *
+ * Returns a new SurfaceObjectTile corresponding to the specified {@code sector}, {@code level}, {@code row},
+ * and {@code column}.
+ *
+ * CAUTION: it is assumed that there exists a single SurfaceShapeTileBuilder. This algorithm might be invalid if there
+ * are more of them (or it might actually work, although it hasn't been tested in that context).
+ *
+ * @param {Sector} sector The tile's Sector.
+ * @param {Level} level The tile's Level in a {@link LevelSet}.
+ * @param {Number} row The tile's row in the Level, starting from 0 and increasing to the right.
+ * @param {Number} column The tile's column in the Level, starting from 0 and increasing upward.
+ *
+ * @return {SurfaceShapeTile} a new SurfaceShapeTile.
+ */
+SurfaceShapeTileBuilder.prototype.createTile = function (sector, level, row, column) {
+ return new SurfaceShapeTile(sector, level, row, column);
+};
+
+SurfaceShapeTileBuilder.prototype.createTopLevelTiles = function () {
+ Tile.createTilesForLevel(this.levels.firstLevel(), this, this.topLevelTiles);
+};
+
+/**
+ * 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 {DrawContext} dc The draw context the surface shape is related to.
+ * @param {SurfaceShapeTile} tile The tile to test for intersection.
+ *
+ * @return {Boolean} true if the tile intersects the draw context's frustum; false otherwise.
+ */
+SurfaceShapeTileBuilder.prototype.intersectsFrustum = function (dc, tile) {
+ if (dc.globe.projectionLimits && !tile.sector.overlaps(dc.globe.projectionLimits)) {
+ return false;
+ }
+
+ tile.update(dc);
+
+ return tile.extent.intersectsFrustum(dc.pickingMode ? dc.pickFrustum : dc.frustumInModelCoordinates);
+};
+
+/**
+ * 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 {DrawContext} dc The current draw context.
+ * @param {LevelSet} levels The level set the tile belongs to.
+ * @param {SurfaceShapeTile} tile The tile to test.
+ *
+ * @return {Boolean} true if the tile meets the rendering criteria; false otherwise.
+ */
+SurfaceShapeTileBuilder.prototype.meetsRenderCriteria = function (dc, levels, tile) {
+ return tile.level.levelNumber == levels.lastLevel().levelNumber || !tile.mustSubdivide(dc, this.detailControl);
+};
+
+/**
+ * Internal use only.
+ * Count of pick operations. This is used to give a surface shape tile a unique pick sequence number if it is
+ * participating in picking.
+ * @type {Number}
+ */
+SurfaceShapeTileBuilder.pickSequence = 0;
+
+export default SurfaceShapeTileBuilder;
diff --git a/web/test/WebWorldWind/src/shapes/Text.js b/web/test/WebWorldWind/src/shapes/Text.js
new file mode 100644
index 00000000..17680f8b
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/Text.js
@@ -0,0 +1,538 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports Text
+ */
+import ArgumentError from '../error/ArgumentError';
+import BasicTextureProgram from '../shaders/BasicTextureProgram';
+import Color from '../util/Color';
+import Font from '../util/Font';
+import Logger from '../util/Logger';
+import Matrix from '../geom/Matrix';
+import PickedObject from '../pick/PickedObject';
+import Renderable from '../render/Renderable';
+import TextAttributes from '../shapes/TextAttributes';
+import UnsupportedOperationError from '../error/UnsupportedOperationError';
+import Vec2 from '../geom/Vec2';
+import Vec3 from '../geom/Vec3';
+import WWMath from '../util/WWMath';
+
+
+/**
+ * Constructs a text shape. This constructor is intended to be called only by subclasses.
+ * @alias Text
+ * @constructor
+ * @augments Renderable
+ * @classdesc Represents a string of text displayed at a specified geographic or screen position.
+ * This is an abstract class meant to be subclassed and not meant to be instantiated directly.
+ * See {@link GeographicText} and {@link ScreenText} for concrete classes.
+ *
+ * @param {String} text The text to display.
+ * @throws {ArgumentError} If the specified text is null or undefined.
+ */
+function Text(text) {
+ if (!text) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Text", "constructor", "missingText"));
+ }
+
+ Renderable.call(this);
+
+ /**
+ * The text's attributes. If null and this text is not highlighted, this text is not drawn.
+ * @type {TextAttributes}
+ * @default see [TextAttributes]{@link TextAttributes}
+ */
+ this.attributes = new TextAttributes(null);
+
+ /**
+ * The attributes used when this text's highlighted flag is true. If null and the
+ * highlighted flag is true, this text's normal attributes are used. If they, too, are null, this
+ * text is not drawn.
+ * @type {TextAttributes}
+ * @default null
+ */
+ this.highlightAttributes = null;
+
+ /**
+ * Indicates whether this text uses its highlight attributes rather than its normal attributes.
+ * @type {boolean}
+ * @default false
+ */
+ this.highlighted = false;
+
+ /**
+ * Indicates whether this text is drawn.
+ * @type {boolean}
+ * @default true
+ */
+ this.enabled = true;
+
+ /**
+ * This shape's text. If null or empty, no text is drawn.
+ * @type {String}
+ * @default null
+ */
+ this.text = text;
+
+ /**
+ * This text's altitude mode. May be one of
+ *
+ *
+ * @default WorldWind.ABSOLUTE
+ */
+ this.altitudeMode = WorldWind.ABSOLUTE;
+
+ /**
+ * Indicates the object to return as the userObject of this text when picked. If null,
+ * then this text object is returned as the userObject.
+ * @type {Object}
+ * @default null
+ * @see [PickedObject.userObject]{@link PickedObject#userObject}
+ */
+ this.pickDelegate = null;
+
+ /**
+ * Indicates whether this text has visual priority over other shapes in the scene.
+ * @type {Boolean}
+ * @default false
+ */
+ this.alwaysOnTop = false;
+
+ /**
+ * This shape's target visibility, a value between 0 and 1. During ordered rendering this shape modifies its
+ * [current visibility]{@link Text#currentVisibility} towards its target visibility at the rate
+ * specified by the draw context's [fadeVelocity]{@link DrawContext#fadeVelocity} property. The target
+ * visibility and current visibility are used to control the fading in and out of this shape.
+ * @type {Number}
+ * @default 1
+ */
+ this.targetVisibility = 1;
+
+ /**
+ * This shape's current visibility, a value between 0 and 1. This property scales the shape's effective
+ * opacity. It is incremented or decremented each frame according to the draw context's
+ * [fade velocity]{@link DrawContext#fadeVelocity} property in order to achieve this shape's current
+ * [target visibility]{@link Text#targetVisibility}. This current visibility and target visibility are
+ * used to control the fading in and out of this shape.
+ * @type {Number}
+ * @default 1
+ * @readonly
+ */
+ this.currentVisibility = 1;
+
+ /**
+ * Indicates the group ID of the declutter group to include this Text shape. If non-zer0, this shape
+ * is decluttered relative to all other shapes within its group.
+ * @type {Number}
+ * @default 0
+ */
+ this.declutterGroup = 0;
+
+ /**
+ * The image to display when this text shape is eliminated from the scene due to decluttering.
+ * @type {String}
+ * @default A round dot drawn in this shape's text color.
+ */
+ this.markerImageSource = WorldWind.configuration.baseUrl + "images/white-dot.png";
+
+ /**
+ * The scale to apply to the [markerImageSource]{@link Text#markerImageSource}.
+ * @type {Number}
+ * @default 0.1
+ */
+ this.markerImageScale = 0.1;
+
+ // Internal use only. Intentionally not documented.
+ this.activeAttributes = null;
+
+ // Internal use only. Intentionally not documented.
+ this.activeTexture = null;
+
+ // Internal use only. Intentionally not documented.
+ this.imageTransform = Matrix.fromIdentity();
+
+ // Internal use only. Intentionally not documented.
+ this.imageBounds = null;
+
+ // Internal use only. Intentionally not documented.
+ this.layer = null;
+
+ // Internal use only. Intentionally not documented.
+ this.depthOffset = -0.003;
+
+ // Internal use only. Intentionally not documented.
+ this.screenPoint = new Vec3(0, 0, 0);
+}
+
+// Internal use only. Intentionally not documented.
+Text.matrix = Matrix.fromIdentity(); // scratch variable
+Text.glPickPoint = new Vec3(0, 0, 0); // scratch variable
+
+Text.prototype = Object.create(Renderable.prototype);
+
+/**
+ * Copies the contents of a specified text object to this text object.
+ * @param {Text} that The text object to copy.
+ */
+Text.prototype.copy = function (that) {
+ this.text = that.text;
+ this.attributes = that.attributes;
+ this.highlightAttributes = that.highlightAttributes;
+ this.highlighted = that.highlighted;
+ this.enabled = that.enabled;
+ this.altitudeMode = that.altitudeMode;
+ this.pickDelegate = that.pickDelegate;
+ this.alwaysOnTop = that.alwaysOnTop;
+ this.depthOffset = that.depthOffset;
+ this.declutterGroup = that.declutterGroup;
+ this.targetVisibility = that.targetVisibility;
+ this.currentVisibility = that.currentVisibility;
+
+ return this;
+};
+
+Object.defineProperties(Text.prototype, {
+ /**
+ * Indicates the screen coordinate bounds of this shape during ordered rendering.
+ * @type {Rectangle}
+ * @readonly
+ * @memberof Text.prototype
+ */
+ screenBounds: {
+ get: function () {
+ return this.imageBounds;
+ }
+ }
+});
+
+/**
+ * Renders this text. This method is typically not called by applications but is called by
+ * [RenderableLayer]{@link RenderableLayer} during rendering. For this shape this method creates and
+ * enques an ordered renderable with the draw context and does not actually draw the text.
+ * @param {DrawContext} dc The current draw context.
+ */
+Text.prototype.render = function (dc) {
+ if (!this.enabled || !this.text || this.text.length === 0) {
+ return;
+ }
+
+ if (!dc.accumulateOrderedRenderables) {
+ return;
+ }
+
+ // Create an ordered renderable for this text. If one has already been created this frame then we're
+ // in 2D-continuous mode and another needs to be created for one of the alternate globe offsets.
+ var orderedText;
+ if (this.lastFrameTime !== dc.timestamp) {
+ orderedText = this.makeOrderedRenderable(dc);
+ } else {
+ var textCopy = this.clone();
+ orderedText = textCopy.makeOrderedRenderable(dc);
+ }
+
+ if (!orderedText) {
+ return;
+ }
+
+ if (!orderedText.isVisible(dc)) {
+ return;
+ }
+
+ orderedText.layer = dc.currentLayer;
+
+ this.lastFrameTime = dc.timestamp;
+ dc.addOrderedRenderable(orderedText);
+};
+
+/**
+ * Draws this shape as an ordered renderable. Applications do not call this function. It is called by
+ * {@link WorldWindow} during rendering. Implements the {@link OrderedRenderable} interface.
+ * @param {DrawContext} dc The current draw context.
+ */
+Text.prototype.renderOrdered = function (dc) {
+ // Optimize away the case of achieved target visibility of 0 and no marker image to display in that case.
+ if (this.currentVisibility === 0 && this.targetVisibility === 0 && !this.markerImageSource) {
+ return;
+ }
+
+ this.drawOrderedText(dc);
+
+ if (dc.pickingMode) {
+ var po = new PickedObject(this.pickColor.clone(), this.pickDelegate ? this.pickDelegate : this,
+ this.position, this.layer, false);
+
+ dc.resolvePick(po);
+ }
+};
+
+// Intentionally not documented.
+Text.prototype.makeOrderedRenderable = function (dc) {
+ var w, h, s,
+ offset;
+
+ this.determineActiveAttributes(dc);
+ if (!this.activeAttributes) {
+ return null;
+ }
+
+ //// Compute the text's screen point and distance to the eye point.
+ if (!this.computeScreenPointAndEyeDistance(dc)) {
+ return null;
+ }
+
+ this.activeTexture = dc.createTextTexture(this.text, this.activeAttributes);
+
+ w = this.activeTexture.imageWidth;
+ h = this.activeTexture.imageHeight;
+ s = this.activeAttributes.scale;
+ offset = this.activeAttributes.offset.offsetForSize(w, h);
+
+ this.imageTransform.setTranslation(
+ this.screenPoint[0] - offset[0] * s,
+ this.screenPoint[1] - offset[1] * s,
+ this.screenPoint[2]);
+
+ this.imageTransform.setScale(w * s, h * s, 1);
+
+ this.imageBounds = WWMath.boundingRectForUnitQuad(this.imageTransform);
+
+ return this;
+};
+
+/**
+ * Computes this shape's screen point and eye distance. Subclasses must override this method.
+ * @param {DrawContext} dc The current draw context.
+ * @returns {Boolean} true if the screen point can be computed, otherwise false.
+ * @protected
+ */
+Text.prototype.computeScreenPointAndEyeDistance = function (dc) {
+ throw new UnsupportedOperationError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Renderable", "render", "abstractInvocation"));
+};
+
+// Internal. Intentionally not documented.
+Text.prototype.determineActiveAttributes = function (dc) {
+ if (this.highlighted && this.highlightAttributes) {
+ this.activeAttributes = this.highlightAttributes;
+ } else {
+ this.activeAttributes = this.attributes;
+ }
+};
+
+// Internal. Intentionally not documented.
+Text.prototype.isVisible = function (dc) {
+ if (dc.pickingMode) {
+ return dc.pickRectangle && this.imageBounds.intersects(dc.pickRectangle);
+ } else {
+ return this.imageBounds.intersects(dc.viewport);
+ }
+};
+
+// Internal. Intentionally not documented.
+Text.prototype.drawOrderedText = function (dc) {
+ this.beginDrawing(dc);
+
+ try {
+ this.doDrawOrderedText(dc);
+ if (!dc.pickingMode) {
+ //this.drawBatchOrderedText(dc);
+ }
+ } finally {
+ this.endDrawing(dc);
+ }
+};
+
+// Internal. Intentionally not documented.
+Text.prototype.drawBatchOrderedText = function (dc) {
+ // Draw any subsequent text in the ordered renderable queue, removing each from the queue as it's
+ // processed. This avoids the overhead of setting up and tearing down OpenGL state for each text shape.
+
+ var or;
+
+ while ((or = dc.peekOrderedRenderable()) && or instanceof Text) {
+ dc.popOrderedRenderable(); // remove it from the queue
+
+ try {
+ or.doDrawOrderedText(dc);
+ } catch (e) {
+ Logger.logMessage(Logger.LEVEL_WARNING, 'Text', 'drawBatchOrderedText',
+ "Error occurred while rendering text using batching: " + e.message);
+ }
+ // Keep going. Render the rest of the ordered renderables.
+ }
+};
+
+// Internal. Intentionally not documented.
+Text.prototype.beginDrawing = function (dc) {
+ var gl = dc.currentGlContext,
+ program;
+
+ dc.findAndBindProgram(BasicTextureProgram);
+
+ // Configure GL to use the draw context's unit quad VBOs for both model coordinates and texture coordinates.
+ // Most browsers can share the same buffer for vertex and texture coordinates, but Internet Explorer requires
+ // that they be in separate buffers, so the code below uses the 3D buffer for vertex coords and the 2D
+ // buffer for texture coords.
+ program = dc.currentProgram;
+ gl.bindBuffer(gl.ARRAY_BUFFER, dc.unitQuadBuffer3());
+ gl.vertexAttribPointer(program.vertexPointLocation, 3, gl.FLOAT, false, 0, 0);
+ gl.bindBuffer(gl.ARRAY_BUFFER, dc.unitQuadBuffer());
+ gl.vertexAttribPointer(program.vertexTexCoordLocation, 2, gl.FLOAT, false, 0, 0);
+ gl.enableVertexAttribArray(program.vertexPointLocation);
+ gl.enableVertexAttribArray(program.vertexTexCoordLocation);
+
+ // Tell the program which texture unit to use.
+ program.loadTextureUnit(gl, gl.TEXTURE0);
+
+ // Turn off color modulation since we want to pick against the text box and not just the text.
+ program.loadModulateColor(gl, false);
+
+ // Suppress depth-buffer writes.
+ gl.depthMask(false);
+};
+
+// Internal. Intentionally not documented.
+Text.prototype.endDrawing = function (dc) {
+ var gl = dc.currentGlContext,
+ program = dc.currentProgram;
+
+ // Restore the default GL vertex attribute state.
+ gl.disableVertexAttribArray(program.vertexPointLocation);
+ gl.disableVertexAttribArray(program.vertexTexCoordLocation);
+
+ // Restore the default GL buffer and texture bindings.
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
+ gl.bindTexture(gl.TEXTURE_2D, null);
+
+ // Restore the default GL depth mask state.
+ gl.depthMask(true);
+};
+
+// Internal. Intentionally not documented.
+Text.prototype.doDrawOrderedText = function (dc) {
+ var gl = dc.currentGlContext,
+ program = dc.currentProgram;
+
+ // Compute the text's current visibility, potentially requesting additional frames.
+ if (!dc.pickingMode && this.currentVisibility !== this.targetVisibility) {
+ var visibilityDelta = (dc.timestamp - dc.previousRedrawTimestamp) / dc.fadeTime;
+ if (this.currentVisibility < this.targetVisibility) {
+ this.currentVisibility = Math.min(1, this.currentVisibility + visibilityDelta);
+ } else {
+ this.currentVisibility = Math.max(0, this.currentVisibility - visibilityDelta);
+ }
+ dc.redrawRequested = true;
+ }
+
+ // Turn off depth testing for the text unless it's been requested.
+ if (!this.activeAttributes.depthTest) {
+ gl.disable(gl.DEPTH_TEST);
+ }
+
+ // Use the text color and opacity. Modulation is done to white to avoid the program's shader from
+ // modifying the text color. When picking, use the pick color, 100% opacity and no texture.
+ if (!dc.pickingMode) {
+ program.loadColor(gl, Color.WHITE);
+ program.loadOpacity(gl, this.layer.opacity * this.currentVisibility);
+ } else {
+ this.pickColor = dc.uniquePickColor();
+ program.loadColor(gl, this.pickColor);
+ program.loadOpacity(gl, 1);
+ program.loadTextureEnabled(gl, false);
+ }
+
+ // When the text is visible, draw the text label.
+ if (this.currentVisibility > 0) {
+ this.drawLabel(dc);
+ }
+
+ // When the text is not visible, draw a marker to indicate that something is there.
+ if (this.currentVisibility < 1 && this.markerImageSource) {
+ this.drawMarker(dc);
+ }
+
+ // Restore the default GL depth test state.
+ if (!this.activeAttributes.depthTest) {
+ gl.enable(gl.DEPTH_TEST);
+ }
+};
+
+// Internal. Intentionally not documented.
+Text.prototype.drawLabel = function (dc) {
+ var gl = dc.currentGlContext,
+ program = dc.currentProgram,
+ textureBound;
+
+ // Use the label texture when not picking.
+ if (!dc.pickingMode && this.activeTexture) {
+ Text.matrix.setToIdentity();
+ Text.matrix.multiplyByTextureTransform(this.activeTexture);
+ textureBound = this.activeTexture.bind(dc); // returns false if texture is null or cannot be bound
+ program.loadTextureEnabled(gl, textureBound);
+ program.loadTextureMatrix(gl, Text.matrix);
+ }
+
+ // Compute and specify the text label's modelview-projection matrix.
+ Text.matrix.copy(dc.screenProjection);
+ Text.matrix.multiplyMatrix(this.imageTransform);
+ program.loadModelviewProjection(gl, Text.matrix);
+
+ // Draw the text as a two-triangle square.
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+};
+
+// Internal. Intentionally not documented.
+Text.prototype.drawMarker = function (dc) {
+ var gl = dc.currentGlContext,
+ program = dc.currentProgram,
+ textureBound;
+
+ var markerTexture = dc.gpuResourceCache.resourceForKey(this.markerImageSource);
+ if (!markerTexture) {
+ dc.gpuResourceCache.retrieveTexture(dc.currentGlContext, this.markerImageSource);
+ return;
+ }
+
+ // Use the marker opacity and texture when not picking.
+ if (!dc.pickingMode) {
+ Text.matrix.setToIdentity();
+ Text.matrix.multiplyByTextureTransform(markerTexture);
+ textureBound = markerTexture.bind(dc); // returns false if texture is null or cannot be bound
+ program.loadTextureEnabled(gl, textureBound);
+ program.loadTextureMatrix(gl, Text.matrix);
+ program.loadOpacity(gl, this.layer.opacity * (1 - this.currentVisibility));
+ }
+
+ // Compute and specify the marker's modelview-projection matrix.
+ var s = this.markerImageScale;
+ Text.matrix.copy(dc.screenProjection);
+ Text.matrix.multiplyByTranslation(
+ this.screenPoint[0] - s * markerTexture.imageWidth / 2,
+ this.screenPoint[1] - s * markerTexture.imageWidth / 2,
+ this.screenPoint[2]);
+ Text.matrix.multiplyByScale(markerTexture.imageWidth * s, markerTexture.imageHeight * s, 1);
+ program.loadModelviewProjection(gl, Text.matrix);
+
+ // Draw the marker as a two-triangle square.
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+};
+
+export default Text;
diff --git a/web/test/WebWorldWind/src/shapes/TextAttributes.js b/web/test/WebWorldWind/src/shapes/TextAttributes.js
new file mode 100644
index 00000000..bfae914a
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/TextAttributes.js
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports TextAttributes
+ */
+import Color from '../util/Color';
+import Font from '../util/Font';
+import Offset from '../util/Offset';
+
+
+/**
+ * Constructs a text attributes bundle.
+ * @alias TextAttributes
+ * @constructor
+ * @classdesc Holds attributes applied to [Text]{@link Text} shapes and [Placemark]{@link Placemark} labels.
+ *
+ * @param {TextAttributes} attributes Attributes to initialize this attributes instance to. May be null,
+ * in which case the new instance contains default attributes.
+ */
+function TextAttributes(attributes) {
+ this._color = attributes ? attributes._color.clone() : Color.WHITE.clone();
+ this._font = attributes ? attributes._font : new Font(14);
+ this._offset = attributes ? attributes._offset
+ : new Offset(WorldWind.OFFSET_FRACTION, 0.5, WorldWind.OFFSET_FRACTION, 0.0);
+ this._scale = attributes ? attributes._scale : 1;
+ this._depthTest = attributes ? attributes._depthTest : false;
+ this._enableOutline = attributes ? attributes._enableOutline : true;
+ this._outlineWidth = attributes ? attributes._outlineWidth : 4;
+ this._outlineColor = attributes ? attributes._outlineColor : new Color(0, 0, 0, 0.5);
+
+ /**
+ * Indicates whether this object's state key is invalid. Subclasses must set this value to true when their
+ * attributes change. The state key will be automatically computed the next time it's requested. This flag
+ * will be set to false when that occurs.
+ * @type {boolean}
+ * @protected
+ */
+ this.stateKeyInvalid = true;
+}
+
+/**
+ * Computes the state key for this attributes object. Subclasses that define additional attributes must
+ * override this method, call it from that method, and append the state of their attributes to its
+ * return value.
+ * @returns {String} The state key for this object.
+ * @protected
+ */
+TextAttributes.prototype.computeStateKey = function () {
+ return "c " + this._color.toHexString(true) +
+ " f " + this._font.toString() +
+ " o " + this._offset.toString() +
+ " s " + this._scale +
+ " dt " + this._depthTest +
+ " eo " + this._enableOutline +
+ " ow " + this._outlineWidth +
+ " oc " + this._outlineColor.toHexString(true);
+};
+
+Object.defineProperties(TextAttributes.prototype, {
+ /**
+ * A string identifying the state of this attributes object. The string encodes the current values of all
+ * this object's properties. It's typically used to validate cached representations of shapes associated
+ * with this attributes object.
+ * @type {String}
+ * @readonly
+ * @memberof TextAttributes.prototype
+ */
+ stateKey: {
+ get: function () {
+ if (this.stateKeyInvalid) {
+ this._stateKey = this.computeStateKey();
+ this.stateKeyInvalid = false;
+ }
+ return this._stateKey;
+ }
+ },
+
+ /**
+ * The text color.
+ * @type {Color}
+ * @default White (1, 1, 1, 1)
+ * @memberof TextAttributes.prototype
+ */
+ color: {
+ get: function () {
+ return this._color;
+ },
+ set: function (value) {
+ this._color = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * The text size, face and other characteristics, as described in [Font]{@link Font}.
+ * @type {Font}
+ * @default Those of [Font]{@link Font}, but with a font size of 14.
+ * @memberof TextAttributes.prototype
+ */
+ font: {
+ get: function () {
+ return this._font;
+ },
+ set: function (value) {
+ this._font = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates the location of the text relative to its specified position.
+ * May be null, in which case the text's bottom-left corner is placed at the specified position.
+ * @type {Offset}
+ * @default 0.5, 0.0, both fractional (Places the text's horizontal center and vertical bottom at the
+ * specified position.)
+ * @memberof TextAttributes.prototype
+ */
+ offset: {
+ get: function () {
+ return this._offset;
+ },
+ set: function (value) {
+ this._offset = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates the amount to scale the text. A value of 0 makes the text disappear.
+ * @type {Number}
+ * @default 1.0
+ * @memberof TextAttributes.prototype
+ */
+ scale: {
+ get: function () {
+ return this._scale;
+ },
+ set: function (value) {
+ this._scale = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates whether the text should be depth-tested against other objects in the scene. If true,
+ * the text may be occluded by terrain and other objects in certain viewing situations. If false,
+ * the text will not be occluded by terrain and other objects.
+ * @type {Boolean}
+ * @default false
+ * @memberof TextAttributes.prototype
+ */
+ depthTest: {
+ get: function () {
+ return this._depthTest;
+ },
+ set: function (value) {
+ this._depthTest = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates if the text will feature an outline around its characters.
+ * @type {Boolean}
+ * @default true
+ * @memberof TextAttributes.prototype
+ */
+ enableOutline: {
+ get: function () {
+ return this._enableOutline;
+ },
+ set: function (value) {
+ this._enableOutline = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * Indicates the text outline width (or thickness) in pixels.
+ * @type {Number}
+ * @default 4
+ * @memberof TextAttributes.prototype
+ */
+ outlineWidth: {
+ get: function () {
+ return this._outlineWidth;
+ },
+ set: function (value) {
+ this._outlineWidth = value;
+ this.stateKeyInvalid = true;
+ }
+ },
+
+ /**
+ * The color of the outline.
+ * @type {Color}
+ * @default Half-transparent black (0, 0, 0, 0.5)
+ * @memberof TextAttributes.prototype
+ */
+ outlineColor: {
+ get: function () {
+ return this._outlineColor;
+ },
+ set: function (value) {
+ this._outlineColor = value;
+ this.stateKeyInvalid = true;
+ }
+ }
+});
+
+export default TextAttributes;
diff --git a/web/test/WebWorldWind/src/shapes/TriangleMesh.js b/web/test/WebWorldWind/src/shapes/TriangleMesh.js
new file mode 100644
index 00000000..f91d2ccb
--- /dev/null
+++ b/web/test/WebWorldWind/src/shapes/TriangleMesh.js
@@ -0,0 +1,384 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports TriangleMesh
+ */
+import AbstractMesh from '../shapes/AbstractMesh';
+import ArgumentError from '../error/ArgumentError';
+import BasicTextureProgram from '../shaders/BasicTextureProgram';
+import BoundingBox from '../geom/BoundingBox';
+import Color from '../util/Color';
+import ImageSource from '../util/ImageSource';
+import Location from '../geom/Location';
+import Logger from '../util/Logger';
+import Matrix from '../geom/Matrix';
+import PickedObject from '../pick/PickedObject';
+import Position from '../geom/Position';
+import ShapeAttributes from '../shapes/ShapeAttributes';
+import SurfacePolygon from '../shapes/SurfacePolygon';
+import Vec2 from '../geom/Vec2';
+import Vec3 from '../geom/Vec3';
+
+
+/**
+ * Constructs a triangle mesh.
+ * @alias TriangleMesh
+ * @constructor
+ * @augments AbstractMesh
+ * @classdesc Represents a 3D triangle mesh.
+ *
+ *
+ * If the latter, the mesh positions' altitudes are ignored. (If the mesh should be draped onto the
+ * terrain, you might want to use {@link SurfacePolygon} instead.)
+ *
+ *
+ * @param {Number} x The offset in the X dimension.
+ * @param {String} yUnits The type of units specified for the Y dimension, assuming a lower-left Y origin.
+ * May be one of the following:
+ *
+ *
+ * @param {Number} y The offset in the Y dimension.
+ */
+function Offset(xUnits, x, yUnits, y) {
+
+ /**
+ * The offset in the X dimension, interpreted according to this instance's xUnits argument.
+ * @type {Number}
+ */
+ this.x = x;
+
+ /**
+ * The offset in the Y dimension, interpreted according to this instance's yUnits argument.
+ * @type {Number}
+ */
+ this.y = y;
+
+ /**
+ * The units of this instance's X offset. See this class' constructor description for a list of the
+ * possible values.
+ * @type {String}
+ */
+ this.xUnits = xUnits;
+
+ /**
+ * The units of this instance's Y offset. See this class' constructor description for a list of the
+ * possible values.
+ * @type {String}
+ */
+ this.yUnits = yUnits;
+}
+
+/**
+ * Creates a new copy of this offset with identical property values.
+ * @returns {Offset} A new offset instance with its property values the same as this one's.
+ */
+Offset.prototype.clone = function () {
+ return new Offset(this.xUnits, this.x, this.yUnits, this.y);
+};
+
+/**
+ * Returns this offset's absolute X and Y coordinates in pixels for a rectangle of a specified size in pixels.
+ * The returned offset is in pixels relative to the rectangle's origin, and is defined in the coordinate
+ * system used by the caller.
+ * @param {Number} width The rectangle's width in pixels.
+ * @param {Number} height The rectangles height in pixels.
+ * @returns {Vec2} The computed offset relative to the rectangle's origin.
+ */
+Offset.prototype.offsetForSize = function (width, height) {
+ var x, y;
+
+ if (this.xUnits === WorldWind.OFFSET_FRACTION) {
+ x = width * this.x;
+ } else if (this.xUnits === WorldWind.OFFSET_INSET_PIXELS) {
+ x = width - this.x;
+ } else { // default to OFFSET_PIXELS
+ x = this.x;
+ }
+
+ if (this.yUnits === WorldWind.OFFSET_FRACTION) {
+ y = height * this.y;
+ } else if (this.yUnits === WorldWind.OFFSET_INSET_PIXELS) {
+ y = height - this.y;
+ } else { // default to OFFSET_PIXELS
+ y = this.y;
+ }
+
+ return new Vec2(x, y);
+};
+
+/**
+ * Returns a string representation of this object.
+ * @returns {String} A string representation of this object.
+ */
+Offset.prototype.toString = function () {
+ return this.xUnits + " " + this.x + " " + this.yUnits + " " + this.y;
+};
+
+export default Offset;
diff --git a/web/test/WebWorldWind/src/util/PolygonSplitter.js b/web/test/WebWorldWind/src/util/PolygonSplitter.js
new file mode 100644
index 00000000..b1ec5e3c
--- /dev/null
+++ b/web/test/WebWorldWind/src/util/PolygonSplitter.js
@@ -0,0 +1,452 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import HashMap from './HashMap';
+import Location from '../geom/Location';
+import Position from '../geom/Position';
+import WWMath from './WWMath';
+
+
+/**
+ * Splits polygons that cross the anti-meridian and/or contain a pole.
+ * @exports PolygonSplitter
+ */
+
+var PolygonSplitter = {
+
+ //Internal
+ //Keeps track of the index of added points so that no point is duplicated
+ addedIndex: -1,
+
+ //Internal
+ //The index where pole insertion began
+ poleIndexOffset: -1,
+
+ /**
+ * Splits an array of polygons that cross the anti-meridian or contain a pole.
+ *
+ * @param {Array} contours an array of arrays of Locations or Positions
+ * Each array entry defines one of this polygon's boundaries.
+ * @param {Array} resultContours an empty array to put the result of the split. Each element will have the
+ * shape of PolygonSplitter.formatContourOutput
+ * @returns {Boolean} true if one of the boundaries crosses the anti-meridian otherwise false
+ * */
+ splitContours: function (contours, resultContours) {
+ var doesCross = false;
+
+ for (var i = 0, len = contours.length; i < len; i++) {
+ var contourInfo = this.splitContour(contours[i]);
+ if (contourInfo.polygons.length > 1) {
+ doesCross = true;
+ }
+ resultContours.push(contourInfo);
+ }
+
+ return doesCross;
+ },
+
+ /**
+ * Splits a polygon that cross the anti-meridian or contain a pole.
+ *
+ * @param {Location[] | Position[]} points an array of Locations or Positions that define a polygon
+ * @returns {Object} @see PolygonSplitter.formatContourOutput
+ * */
+ splitContour: function (points) {
+ var iMap = new HashMap();
+ var newPoints = [];
+ var intersections = [];
+ var polygons = [];
+ var iMaps = [];
+ var poleIndex = -1;
+
+ var pole = this.findIntersectionAndPole(points, newPoints, intersections, iMap);
+
+ if (intersections.length === 0) {
+ polygons.push(newPoints);
+ iMaps.push(iMap);
+ return this.formatContourOutput(polygons, pole, poleIndex, iMaps);
+ }
+
+ if (intersections.length > 2) {
+ intersections.sort(function (a, b) {
+ return b.latitude - a.latitude;
+ });
+ }
+
+ if (pole !== Location.poles.NONE) {
+ newPoints = this.handleOnePole(newPoints, intersections, iMap, pole);
+ iMap = this.reindexIntersections(intersections, iMap, this.poleIndexOffset);
+ }
+ if (intersections.length === 0) {
+ polygons.push(newPoints);
+ iMaps.push(iMap);
+ poleIndex = 0;
+ return this.formatContourOutput(polygons, pole, poleIndex, iMaps);
+ }
+
+ this.linkIntersections(intersections, iMap);
+
+ poleIndex = this.makePolygons(newPoints, intersections, iMap, polygons, iMaps);
+
+ return this.formatContourOutput(polygons, pole, poleIndex, iMaps);
+ },
+
+ /**
+ * Internal. Applications should not call this method.
+ * Finds the intersections with the anti-meridian and if the polygon contains one of the poles.
+ * A new polygon is constructed with the intersections and pole points and stored in newPoints
+ *
+ * @param {Location[] | Position[]} points An array of Locations or Positions that define a polygon
+ * @param {Location[] | Position[]} newPoints An empty array where to store the resulting polygon with intersections
+ * @param {Array} intersections An empty array where to store the intersection latitude and index
+ * @param {HashMap} iMap A hashMap to store intersection data
+ * The key is the index in the newPoints array and value is PolygonSplitter.makeIntersectionEntry
+ * @returns {Number} The pole number @see Location.poles
+ * */
+ findIntersectionAndPole: function (points, newPoints, intersections, iMap) {
+ var containsPole = false;
+ var minLatitude = 90.0;
+ var maxLatitude = -90.0;
+ this.addedIndex = -1;
+
+ for (var i = 0, lenC = points.length; i < lenC; i++) {
+ var pt1 = points[i];
+ var pt2 = points[(i + 1) % lenC];
+
+ minLatitude = Math.min(minLatitude, pt1.latitude);
+ maxLatitude = Math.max(maxLatitude, pt1.latitude);
+
+ var doesCross = Location.locationsCrossDateLine([pt1, pt2]);
+ if (doesCross) {
+ containsPole = !containsPole;
+
+ var iLatitude = Location.meridianIntersection(pt1, pt2, 180);
+ if (iLatitude === null) {
+ iLatitude = (pt1.latitude + pt2.latitude) / 2;
+ }
+ var iLongitude = WWMath.signum(pt1.longitude) * 180 || 180;
+
+ var iLoc1 = this.createPoint(iLatitude, iLongitude, pt1.altitude);
+ var iLoc2 = this.createPoint(iLatitude, -iLongitude, pt2.altitude);
+
+ this.safeAdd(newPoints, pt1, i, lenC);
+
+ var index = newPoints.length;
+ iMap.set(index, this.makeIntersectionEntry(index));
+ iMap.set(index + 1, this.makeIntersectionEntry(index + 1));
+ intersections.push({
+ indexEnd: index,
+ indexStart: index + 1,
+ latitude: iLatitude
+ });
+
+ newPoints.push(iLoc1);
+ newPoints.push(iLoc2);
+
+ this.safeAdd(newPoints, pt2, i + 1, lenC);
+ }
+ else {
+ this.safeAdd(newPoints, pt1, i, lenC);
+ this.safeAdd(newPoints, pt2, i + 1, lenC);
+ }
+ }
+
+ var pole = Location.poles.NONE;
+ if (containsPole) {
+ pole = this.determinePole(minLatitude, maxLatitude);
+ }
+
+ return pole;
+ },
+
+ /**
+ * Internal. Applications should not call this method.
+ * 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.
+ * @param {Number} minLatitude The minimum latitude of a polygon that contains a pole
+ * @param {Number} maxLatitude The maximum latitude of a polygon that contains a pole
+ * @returns {Number} The pole number @see Location.poles
+ * */
+ determinePole: function (minLatitude, maxLatitude) {
+ var pole;
+ if (minLatitude > 0) {
+ pole = Location.poles.NORTH; // Entirely in Northern Hemisphere.
+ }
+ else if (maxLatitude < 0) {
+ pole = Location.poles.SOUTH; // Entirely in Southern Hemisphere.
+ }
+ else if (Math.abs(maxLatitude) >= Math.abs(minLatitude)) {
+ pole = Location.poles.NORTH; // Spans equator, but more north than south.
+ }
+ else {
+ pole = Location.poles.SOUTH; // Spans equator, but more south than north.
+ }
+ return pole;
+ },
+
+ /**
+ * Internal. Applications should not call this method.
+ * Creates a new array of points containing the two pole locations on both sides of the anti-meridian
+ *
+ * @param {Location[] | Position[]} points
+ * @param {Array} intersections
+ * @param {HashMap} iMap
+ * @param {Number} pole
+ * @return {Object} an object containing the new points and a new reIndexed iMap
+ * */
+ handleOnePole: function (points, intersections, iMap, pole) {
+ var pointsClone;
+
+ if (pole === Location.poles.NORTH) {
+ var intersection = intersections.shift();
+ var poleLat = 90;
+ }
+ else if (pole === Location.poles.SOUTH) {
+ intersection = intersections.pop();
+ poleLat = -90;
+ }
+
+ var iEnd = iMap.get(intersection.indexEnd);
+ var iStart = iMap.get(intersection.indexStart);
+ iEnd.forPole = true;
+ iStart.forPole = true;
+
+ this.poleIndexOffset = intersection.indexStart;
+
+ pointsClone = points.slice(0, intersection.indexEnd + 1);
+ var polePoint1 = this.createPoint(poleLat, points[iEnd.index].longitude, points[iEnd.index].altitude);
+ var polePoint2 = this.createPoint(poleLat, points[iStart.index].longitude, points[iStart.index].altitude);
+ pointsClone.push(polePoint1, polePoint2);
+ pointsClone = pointsClone.concat(points.slice(this.poleIndexOffset));
+
+ return pointsClone;
+ },
+
+ /**
+ * Internal. Applications should not call this method.
+ * Links adjacents pairs of intersection by index
+ * @param {Array} intersections
+ * @param {HashMap} iMap
+ * */
+ linkIntersections: function (intersections, iMap) {
+ for (var i = 0; i < intersections.length - 1; i += 2) {
+ var i0 = intersections[i];
+ var i1 = intersections[i + 1];
+
+ var iEnd0 = iMap.get(i0.indexEnd);
+ var iStart0 = iMap.get(i0.indexStart);
+ var iEnd1 = iMap.get(i1.indexEnd);
+ var iStart1 = iMap.get(i1.indexStart);
+
+ iEnd0.linkTo = i1.indexStart;
+ iStart0.linkTo = i1.indexEnd;
+ iEnd1.linkTo = i0.indexStart;
+ iStart1.linkTo = i0.indexEnd;
+ }
+ },
+
+ /**
+ * Internal. Applications should not call this method.
+ * ReIndexes the intersections due to the poles being added to the array of points
+ * @param {Array} intersections
+ * @param {HashMap} iMap
+ * @param {Number} indexOffset the index from which to start reIndexing
+ * @returns {HashMap} a new hash map with the correct indices
+ * */
+ reindexIntersections: function (intersections, iMap, indexOffset) {
+ iMap = HashMap.reIndex(iMap, indexOffset, 2);
+
+ for (var i = 0, len = intersections.length; i < len; i++) {
+ if (intersections[i].indexEnd >= indexOffset) {
+ intersections[i].indexEnd += 2;
+ }
+ if (intersections[i].indexStart >= indexOffset) {
+ intersections[i].indexStart += 2;
+ }
+ }
+
+ return iMap;
+ },
+
+ /**
+ * Internal. Applications should not call this method.
+ * @param {Location[] | Position[]} points
+ * @param {Array} intersections
+ * @param {HashMap} iMap
+ * @param {Array} polygons an empty array to store the resulting polygons
+ * @param {HashMap[]} iMaps an empty array to store the resulting hasp maps for each polygon
+ * @returns {Number} the pole number @see Location.poles
+ * */
+ makePolygons: function (points, intersections, iMap, polygons, iMaps) {
+ var poleIndex = -1;
+ for (var i = 0; i < intersections.length - 1; i += 2) {
+ var i0 = intersections[i];
+ var i1 = intersections[i + 1];
+
+ var start = i0.indexStart;
+ var end = i1.indexEnd;
+ var polygon = [];
+ var polygonHashMap = new HashMap();
+ var containsPole = this.makePolygon(start, end, points, iMap, polygon, polygonHashMap);
+ if (polygon.length) {
+ polygons.push(polygon);
+ iMaps.push(polygonHashMap);
+ if (containsPole) {
+ poleIndex = polygons.length - 1;
+ }
+ }
+
+ start = i1.indexStart;
+ end = i0.indexEnd;
+ polygon = [];
+ polygonHashMap = new HashMap();
+ containsPole = this.makePolygon(start, end, points, iMap, polygon, polygonHashMap);
+ if (polygon.length) {
+ polygons.push(polygon);
+ iMaps.push(polygonHashMap);
+ if (containsPole) {
+ poleIndex = polygons.length - 1;
+ }
+ }
+ }
+
+ return poleIndex;
+ },
+
+ /**
+ * Internal. Applications should not call this method.
+ * Paths from a start intersection index to an end intersection index and makes a polygon and a hashMap
+ * with the intersection indices
+ * @param {Number} start the index of a start type intersection
+ * @param {Number} end the index of an end type intersection
+ * @param {Location[] | Position[]} points
+ * @param {HashMap} iMap
+ * @param {Location[] | Position[]} resultPolygon an empty array to store the resulting polygon
+ * @param {HashMap} polygonHashMap a hash map to record the indices of the intersections in the polygon
+ * @returns {Boolean} true if the polygon contains a pole
+ * */
+ makePolygon: function (start, end, points, iMap, resultPolygon, polygonHashMap) {
+ var pass = false;
+ var len = points.length;
+ var containsPole = false;
+
+ if (end < start) {
+ end += len;
+ }
+
+ for (var i = start; i <= end; i++) {
+ var idx = i % len;
+ var pt = points[idx];
+ var intersection = iMap.get(idx);
+
+ if (intersection) {
+ if (intersection.visited) {
+ break;
+ }
+
+ resultPolygon.push(pt);
+ polygonHashMap.set(resultPolygon.length - 1, intersection);
+
+ if (intersection.forPole) {
+ containsPole = true;
+ }
+ else {
+ if (pass) {
+ i = intersection.linkTo - 1;
+ if (i + 1 === start) {
+ break;
+ }
+ }
+ pass = !pass;
+ intersection.visited = true;
+ }
+ }
+ else {
+ resultPolygon.push(pt);
+ }
+ }
+
+ return containsPole;
+ },
+
+ /**
+ * Internal. Applications should not call this method.
+ * Adds an element to an array preventing duplication
+ * @param {Location[] | Position[]} points
+ * @param {Location | Position} point
+ * @param {Number} index The index of the Point from the source array
+ * @param {Number} len The length of the source array
+ * */
+ safeAdd: function (points, point, index, len) {
+ if (this.addedIndex < index && this.addedIndex < len - 1) {
+ points.push(point);
+ this.addedIndex = index;
+ }
+ },
+
+ /**
+ * Internal. Applications should not call this method.
+ * Creates a Location or a Position
+ * @param {Number} latitude
+ * @param {Number} longitude
+ * @param {Number} altitude
+ * @returns Location | Position
+ * */
+ createPoint: function (latitude, longitude, altitude) {
+ if (altitude == null) {
+ return new Location(latitude, longitude);
+ }
+ return new Position(latitude, longitude, altitude);
+ },
+
+ /**
+ * Internal. Applications should not call this method.
+ * @param {Array} polygons an array of arrays of Locations or Positions
+ * @param {Number} pole the pole number @see Location.poles
+ * @param {Number} poleIndex the index of the polygon containing the pole
+ * @param {HashMap[]} iMaps an array of hash maps for each polygon
+ * */
+ formatContourOutput: function (polygons, pole, poleIndex, iMaps) {
+ return {
+ polygons: polygons,
+ pole: pole,
+ poleIndex: poleIndex,
+ iMap: iMaps
+ };
+ },
+
+ /**
+ * Internal. Applications should not call this method.
+ * @param {Number} index the index of the intersection in the array of points
+ * */
+ makeIntersectionEntry: function (index) {
+ if (index == null) {
+ index = -1;
+ }
+ return {
+ visited: false,
+ forPole: false,
+ index: index,
+ linkTo: -1
+ }
+ }
+};
+
+export default PolygonSplitter;
+
+
diff --git a/web/test/WebWorldWind/src/util/SunPosition.js b/web/test/WebWorldWind/src/util/SunPosition.js
new file mode 100644
index 00000000..06253c71
--- /dev/null
+++ b/web/test/WebWorldWind/src/util/SunPosition.js
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Angle from '../geom/Angle';
+import ArgumentError from '../error/ArgumentError';
+import Logger from './Logger';
+import WWMath from './WWMath';
+
+
+/**
+ * Provides utilities for determining the Sun geographic and celestial location.
+ * @exports SunPosition
+ */
+var SunPosition = {
+
+ /**
+ * Computes the geographic location of the sun for a given date
+ * @param {Date} date
+ * @throws {ArgumentError} if the date is missing
+ * @return {{latitude: Number, longitude: Number}} the geographic location
+ */
+ getAsGeographicLocation: function (date) {
+ if (date instanceof Date === false) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "SunPosition", "getAsGeographicLocation",
+ "missingDate"));
+ }
+
+ var celestialLocation = this.getAsCelestialLocation(date);
+ return this.celestialToGeographic(celestialLocation, date);
+ },
+
+ /**
+ * Computes the celestial location of the sun for a given julianDate
+ * @param {Date} date
+ * @throws {ArgumentError} if the date is missing
+ * @return {{declination: Number, rightAscension: Number}} the celestial location
+ */
+ getAsCelestialLocation: function (date) {
+ if (date instanceof Date === false) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "SunPosition", "getAsCelestialLocation",
+ "missingDate"));
+ }
+
+ var julianDate = this.computeJulianDate(date);
+
+ //number of days (positive or negative) since Greenwich noon, Terrestrial Time, on 1 January 2000 (J2000.0)
+ var numDays = julianDate - 2451545;
+
+ var meanLongitude = WWMath.normalizeAngle360(280.460 + 0.9856474 * numDays);
+
+ var meanAnomaly = WWMath.normalizeAngle360(357.528 + 0.9856003 * numDays) * Angle.DEGREES_TO_RADIANS;
+
+ var eclipticLongitude = meanLongitude + 1.915 * Math.sin(meanAnomaly) + 0.02 * Math.sin(2 * meanAnomaly);
+ var eclipticLongitudeRad = eclipticLongitude * Angle.DEGREES_TO_RADIANS;
+
+ var obliquityOfTheEcliptic = (23.439 - 0.0000004 * numDays) * Angle.DEGREES_TO_RADIANS;
+
+ var declination = Math.asin(Math.sin(obliquityOfTheEcliptic) * Math.sin(eclipticLongitudeRad)) *
+ Angle.RADIANS_TO_DEGREES;
+
+ var rightAscension = Math.atan(Math.cos(obliquityOfTheEcliptic) * Math.tan(eclipticLongitudeRad)) *
+ Angle.RADIANS_TO_DEGREES;
+
+ //compensate for atan result
+ if (eclipticLongitude >= 90 && eclipticLongitude < 270) {
+ rightAscension += 180;
+ }
+ rightAscension = WWMath.normalizeAngle360(rightAscension);
+
+ return {
+ declination: declination,
+ rightAscension: rightAscension
+ };
+ },
+
+ /**
+ * Converts from celestial coordinates (declination and right ascension) to geographic coordinates
+ * (latitude, longitude) for a given julian date
+ * @param {{declination: Number, rightAscension: Number}} celestialLocation
+ * @param {Date} date
+ * @throws {ArgumentError} if celestialLocation or julianDate are missing
+ * @return {{latitude: Number, longitude: Number}} the geographic location
+ */
+ celestialToGeographic: function (celestialLocation, date) {
+ if (!celestialLocation) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "SunPosition", "celestialToGeographic",
+ "missingCelestialLocation"));
+ }
+ if (date instanceof Date === false) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "SunPosition", "celestialToGeographic", "missingDate"));
+ }
+
+ var julianDate = this.computeJulianDate(date);
+
+ //number of days (positive or negative) since Greenwich noon, Terrestrial Time, on 1 January 2000 (J2000.0)
+ var numDays = julianDate - 2451545;
+
+ //Greenwich Mean Sidereal Time
+ var GMST = WWMath.normalizeAngle360(280.46061837 + 360.98564736629 * numDays);
+
+ //Greenwich Hour Angle
+ var GHA = WWMath.normalizeAngle360(GMST - celestialLocation.rightAscension);
+
+ var longitude = Angle.normalizedDegreesLongitude(-GHA);
+
+ return {
+ latitude: celestialLocation.declination,
+ longitude: longitude
+ };
+ },
+
+ /**
+ * Computes the julian date from a javascript date object
+ * @param {Date} date
+ * @throws {ArgumentError} if the date is missing
+ * @return {Number} the julian date
+ */
+ computeJulianDate: function (date) {
+ if (date instanceof Date === false) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "SunPosition", "computeJulianDate", "missingDate"));
+ }
+
+ var year = date.getUTCFullYear();
+ var month = date.getUTCMonth() + 1;
+ var day = date.getUTCDate();
+ var hour = date.getUTCHours();
+ var minute = date.getUTCMinutes();
+ var second = date.getUTCSeconds();
+
+ var dayFraction = (hour + minute / 60 + second / 3600) / 24;
+
+ if (month <= 2) {
+ year -= 1;
+ month += 12;
+ }
+
+ var A = Math.floor(year / 100);
+ var B = 2 - A + Math.floor(A / 4);
+ var JD0h = Math.floor(365.25 * (year + 4716)) + Math.floor(30.6001 * (month + 1)) + day + B - 1524.5;
+
+ return JD0h + dayFraction;
+ }
+
+};
+
+export default SunPosition;
+
diff --git a/web/test/WebWorldWind/src/util/Tile.js b/web/test/WebWorldWind/src/util/Tile.js
new file mode 100644
index 00000000..1d857b34
--- /dev/null
+++ b/web/test/WebWorldWind/src/util/Tile.js
@@ -0,0 +1,570 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports Tile
+ */
+import ArgumentError from '../error/ArgumentError';
+import BoundingBox from '../geom/BoundingBox';
+import Logger from '../util/Logger';
+import Sector from '../geom/Sector';
+import Vec3 from '../geom/Vec3';
+import WWMath from '../util/WWMath';
+import WWUtil from '../util/WWUtil';
+
+
+/**
+ * Constructs a tile for a specified sector, level, row and column.
+ * @alias Tile
+ * @constructor
+ * @classdesc Represents a tile of terrain or imagery.
+ * Provides a base class for texture tiles used by tiled image layers and elevation tiles used by elevation models.
+ * Applications typically do not interact with this class.
+ * @param {Sector} sector The sector represented by this tile.
+ * @param {Level} level This tile's level in a tile pyramid.
+ * @param {Number} row This tile's row in the specified level in a tile pyramid.
+ * @param {Number} column This tile's column in the specified level in a tile pyramid.
+ * @throws {ArgumentError} If the specified sector or level is null or undefined or the row or column arguments
+ * are less than zero.
+ */
+function Tile(sector, level, row, column) {
+ if (!sector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "constructor", "missingSector"));
+ }
+
+ if (!level) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "constructor",
+ "The specified level is null or undefined."));
+ }
+
+ if (row < 0 || column < 0) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "constructor",
+ "The specified row or column is less than zero."));
+ }
+
+ /**
+ * The sector represented by this tile.
+ * @type {Sector}
+ * @readonly
+ */
+ this.sector = sector;
+
+ /**
+ * The level at which this tile lies in a tile pyramid.
+ * @type {Number}
+ * @readonly
+ */
+ this.level = level;
+
+ /**
+ * The row in this tile's level in which this tile lies in a tile pyramid.
+ * @type {Number}
+ * @readonly
+ */
+ this.row = row;
+
+ /**
+ * The column in this tile's level in which this tile lies in a tile pyramid.
+ * @type {Number}
+ * @readonly
+ */
+ this.column = column;
+
+ /**
+ * The width in pixels or cells of this tile's associated resource.
+ * @type {Number}
+ */
+ this.tileWidth = level.tileWidth;
+
+ /**
+ * The height in pixels or cells of this tile's associated resource.
+ * @type {Number}
+ */
+ this.tileHeight = level.tileHeight;
+
+ /**
+ * The size in radians of pixels or cells of this tile's associated resource.
+ * @type {Number}
+ */
+ this.texelSize = level.texelSize;
+
+ /**
+ * A key that uniquely identifies this tile within a level set.
+ * @type {String}
+ * @readonly
+ */
+ this.tileKey = Tile.computeTileKey(level.levelNumber, row, column);
+
+ /**
+ * The Cartesian bounding box of this tile.
+ * @type {BoundingBox}
+ */
+ this.extent = null;
+
+ /**
+ * The tile's local origin in model coordinates. Any model coordinate points associates with the tile
+ * should be relative to this point.
+ * @type {Vec3}
+ */
+ this.referencePoint = null;
+
+ /**
+ * This tile's opacity.
+ * @type {Number}
+ * @default 1
+ */
+ this.opacity = 1;
+
+ // Internal use only. Intentionally not documented.
+ this.samplePoints = null;
+
+ // Internal use only. Intentionally not documented.
+ this.sampleElevations = null;
+
+ // Internal use only. Intentionally not documented.
+ this.updateTimestamp = null;
+
+ // Internal use only. Intentionally not documented.
+ this.updateVerticalExaggeration = null;
+
+ // Internal use only. Intentionally not documented.
+ this.updateGlobeStateKey = null;
+}
+
+/**
+ * Indicates whether this tile is equivalent to a specified tile.
+ * @param {Tile} that The tile to check equivalence with.
+ * @returns {boolean} true if this tile is equivalent to the specified one, false if
+ * they are not equivalent or the specified tile is null or undefined.
+ */
+Tile.prototype.isEqual = function (that) {
+ if (!that)
+ return false;
+
+ if (!that.tileKey)
+ return false;
+
+ return this.tileKey == that.tileKey;
+};
+
+/**
+ * Returns the size of this tile in bytes.
+ * @returns {Number} The size of this tile in bytes.
+ */
+Tile.prototype.size = function () {
+ return 4 // child pointer
+ + (4 + 32) // sector
+ + 4 //level pointer (the level is common to the layer or tessellator so is not included here)
+ + 8 // row and column
+ + 8 // texel size
+ + (4 + 32) // reference point
+ + (4 + 676) // bounding box
+ + 8 // min and max height
+ + (4 + 32) // nearest point
+ + 8; // extent timestamp and vertical exaggeration
+};
+
+/**
+ * Computes an approximate distance from this tile to a specified vector.
+ * @param {Vec3} vector The vector to compute the distance to.
+ * @returns {number} The distance between this tile and the vector.
+ * @throws {ArgumentError} If the specified vector is null or undefined.
+ */
+Tile.prototype.distanceTo = function (vector) {
+ if (!vector) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "distanceTo", "missingVector"));
+ }
+
+ var px = vector[0], py = vector[1], pz = vector[2],
+ dx, dy, dz,
+ points = this.samplePoints,
+ distance = Number.POSITIVE_INFINITY;
+
+ for (var i = 0, len = points.length; i < len; i += 3) {
+ dx = px - points[i];
+ dy = py - points[i + 1];
+ dz = pz - points[i + 2];
+ distance = Math.min(distance, dx * dx + dy * dy + dz * dz); // minimum squared distance
+ }
+
+ return Math.sqrt(distance);
+};
+
+/**
+ * Returns the four children formed by subdividing this tile.
+ * @param {Level} level The level of the children.
+ * @param {TileFactory} tileFactory The tile factory to use to create the children.
+ * @returns {Tile[]} An array containing the four child tiles.
+ * @throws {ArgumentError} If the specified tile factory or level is null or undefined.
+ */
+Tile.prototype.subdivide = function (level, tileFactory) {
+ if (!level) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "subdivide",
+ "The specified level is null or undefined."));
+ }
+
+ if (!tileFactory) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "subdivide",
+ "The specified tile factory is null or undefined."));
+ }
+
+ var latMin = this.sector.minLatitude,
+ latMax = this.sector.maxLatitude,
+ latMid = this.sector.centroidLatitude(),
+
+ lonMin = this.sector.minLongitude,
+ lonMax = this.sector.maxLongitude,
+ lonMid = this.sector.centroidLongitude(),
+
+ subRow,
+ subCol,
+ childSector,
+ children = [];
+
+ subRow = 2 * this.row;
+ subCol = 2 * this.column;
+ childSector = new Sector(latMid, latMax, lonMin, lonMid);
+ children.push(tileFactory.createTile(childSector, level, subRow, subCol));
+
+ subRow = 2 * this.row;
+ subCol = 2 * this.column + 1;
+ childSector = new Sector(latMid, latMax, lonMid, lonMax);
+ children.push(tileFactory.createTile(childSector, level, subRow, subCol));
+
+ subRow = 2 * this.row + 1;
+ subCol = 2 * this.column;
+ childSector = new Sector(latMin, latMid, lonMin, lonMid);
+ children.push(tileFactory.createTile(childSector, level, subRow, subCol));
+
+ subRow = 2 * this.row + 1;
+ subCol = 2 * this.column + 1;
+ childSector = new Sector(latMin, latMid, lonMid, lonMax);
+ children.push(tileFactory.createTile(childSector, level, subRow, subCol));
+
+ return children;
+};
+
+/**
+ * Returns the four children formed by subdividing this tile, drawing those children from a specified cache
+ * if they exist there.
+ * @param {Level} level The level of the children.
+ * @param {TileFactory} tileFactory The tile factory to use to create the children.
+ * @param {MemoryCache} cache A memory cache that may contain pre-existing child tiles. If non-null, the
+ * cache is checked for a child collection prior to creating that tile. If one exists
+ * in the cache it is returned rather than creating a new collection of children. If a new collection is
+ * created, it is added to the cache.
+ * @returns {Tile[]} An array containing the four tiles.
+ * @throws {ArgumentError} If the specified tile factory or level is null or undefined.
+ */
+Tile.prototype.subdivideToCache = function (level, tileFactory, cache) {
+ if (!level) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "subdivideToCache",
+ "The specified level is null or undefined."));
+ }
+
+ if (!tileFactory) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "subdivideToCache",
+ "The specified tile factory is null or undefined."));
+ }
+
+ var childList = cache ? cache.entryForKey(this.tileKey) : null;
+ if (!childList) {
+ childList = this.subdivide(level, tileFactory);
+ if (childList && cache) {
+ cache.putEntry(this.tileKey, childList, 4 * childList[0].size());
+ }
+ }
+
+ return childList;
+};
+
+/**
+ * Indicates whether this tile should be subdivided based on the current navigation state and a specified
+ * detail factor.
+ * @param {DrawContext} dc The current draw context.
+ * @param {Number} detailFactor The detail factor to consider.
+ * @returns {boolean} true If the tile should be subdivided, otherwise false.
+ */
+Tile.prototype.mustSubdivide = function (dc, detailFactor) {
+ // Split when the cell height (length of a texel) becomes greater than the specified fraction of the eye
+ // distance. The fraction is specified as a power of 10. For example, a detail factor of 3 means split when
+ // the cell height becomes more than one thousandth of the eye distance. Another way to say it is, use the
+ // current tile if the cell height 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.
+
+ var cellSize = dc.globe.equatorialRadius * this.texelSize,
+ distance = this.distanceTo(dc.eyePoint),
+ pixelSize = dc.pixelSizeAtDistance(distance);
+
+ return cellSize > Math.max(detailFactor * pixelSize, 0.5);
+};
+
+/**
+ * Updates this tile's frame-dependent properties as necessary, according to the specified draw context.
+ * update must be called once per frame before the extent and any other frame-dependent
+ * properties are used. update intelligently determines when it is necessary to recompute these
+ * properties, and does nothing if the state of all dependencies has not changed since the last call.
+ * @param {DrawContext} dc The current draw context.
+ */
+Tile.prototype.update = function (dc) {
+ var elevationTimestamp = dc.globe.elevationTimestamp(),
+ verticalExaggeration = dc.verticalExaggeration,
+ globeStateKey = dc.globeStateKey;
+
+ if (this.updateTimestamp != elevationTimestamp
+ || this.updateVerticalExaggeration != verticalExaggeration
+ || this.updateGlobeStateKey != globeStateKey) {
+
+ this.doUpdate(dc);
+ dc.frameStatistics.incrementTileUpdateCount(1);
+
+ // Set the geometry extent to the globe's elevation timestamp on which the geometry is based. This
+ // ensures that the geometry timestamp can be reliably compared to the elevation timestamp in subsequent
+ // frames.
+ this.updateTimestamp = elevationTimestamp;
+ this.updateVerticalExaggeration = verticalExaggeration;
+ this.updateGlobeStateKey = globeStateKey;
+ }
+};
+
+/**
+ * Updates this tile's frame-dependent properties according to the specified draw context.
+ * @param {DrawContext} dc The current draw context.
+ * @protected
+ */
+Tile.prototype.doUpdate = function (dc) {
+ // Compute the minimum and maximum world coordinate height for this tile's sector by multiplying the minimum
+ // and maximum elevations by the scene's vertical exaggeration. This ensures that the elevations to used
+ // build the terrain are contained by this tile's extent. Use zero if the globe as no elevations in this
+ // tile's sector.
+ var globe = dc.globe,
+ verticalExaggeration = dc.verticalExaggeration,
+ extremes = globe.minAndMaxElevationsForSector(this.sector),
+ minHeight = extremes[0] * verticalExaggeration,
+ maxHeight = extremes[1] * verticalExaggeration;
+
+ if (minHeight === maxHeight) {
+ minHeight = maxHeight + 10; // TODO: Determine if this is necessary.
+ }
+
+ // Compute a bounding box for this tile that contains the terrain surface in the tile's coverage area.
+ if (!this.extent) {
+ this.extent = new BoundingBox();
+ }
+ this.extent.setToSector(this.sector, globe, minHeight, maxHeight);
+
+ // Compute the cartesian points for a 3x3 geographic grid. This grid captures sufficiently close sample
+ // points in order to estimate the distance from the viewer to this tile.
+ if (!this.samplePoints) {
+ this.sampleElevations = new Float64Array(9);
+ this.samplePoints = new Float64Array(3 * this.sampleElevations.length);
+ }
+ WWUtil.fillArray(this.sampleElevations, 0.5 * (minHeight + maxHeight));
+ globe.computePointsForGrid(this.sector, 3, 3, this.sampleElevations, Vec3.ZERO, this.samplePoints);
+
+ // Compute the reference point used as a local coordinate origin for the tile.
+ if (!this.referencePoint) {
+ this.referencePoint = new Vec3(0, 0, 0);
+ }
+
+ globe.computePointFromPosition(WWMath.mercatorLatInvert(this.sector.centroidLatitude()), this.sector.centroidLongitude(), 0,
+ this.referencePoint);
+};
+
+/**
+ * Computes a key that uniquely identifies a tile within its level set.
+ *
+ * @param {Number} levelNumber The tile's level number in a tile pyramid.
+ * @param {Number} row The tile's row in the specified level in a tile pyramid.
+ * @param {Number} column The tile's column in the specified level in a tile pyramid.
+ * @returns {String} A string key uniquely identifying a tile with the specified level, row, and column.
+ */
+Tile.computeTileKey = function (levelNumber, row, column) {
+ return levelNumber + "." + row + "." + column;
+};
+
+/**
+ * Computes a row number for a tile within a level given the tile's latitude.
+ * @param {Number} delta The level's latitudinal tile delta in degrees.
+ * @param {Number} latitude The tile's minimum latitude.
+ * @returns {Number} The computed row number.
+ */
+Tile.computeRow = function (delta, latitude) {
+ var row = Math.floor((180 - latitude) / delta);
+
+ // If latitude is at the end of the grid, subtract 1 from the computed row to return the last row.
+ if (latitude == -180) {
+ row -= 1;
+ }
+
+ return row;
+};
+
+/**
+ * Computes a column number for a tile within a level given the tile's longitude.
+ * @param {Number} delta The level's longitudinal tile delta in degrees.
+ * @param {Number} longitude The tile's minimum longitude.
+ * @returns {Number} The computed column number.
+ */
+Tile.computeColumn = function (delta, longitude) {
+ var col = Math.floor((longitude + 180) / delta);
+
+ // If longitude is at the end of the grid, subtract 1 from the computed column to return the last column.
+ if (longitude == 180) {
+ col -= 1;
+ }
+
+ return col;
+};
+
+/**
+ * Computes the last row number for a tile within a level given the tile's maximum latitude.
+ * @param {Number} delta The level's latitudinal tile delta in degrees.
+ * @param {Number} maxLatitude The tile's maximum latitude in degrees.
+ * @returns {Number} The computed row number.
+ */
+Tile.computeLastRow = function (delta, maxLatitude) {
+ var row = Math.ceil((maxLatitude + 180) / delta - 1);
+
+ // If max latitude is in the first row, set the max row to 0.
+ if (maxLatitude + 180 < delta) {
+ row = 0;
+ }
+
+ return row;
+};
+
+/**
+ * Computes the last column number for a tile within a level given the tile's maximum longitude.
+ * @param {Number} delta The level's longitudinal tile delta in degrees.
+ * @param {Number} maxLongitude The tile's maximum longitude in degrees.
+ * @returns {Number} The computed column number.
+ */
+Tile.computeLastColumn = function (delta, maxLongitude) {
+ var col = Math.ceil((maxLongitude + 180) / delta - 1);
+
+ // If max longitude is in the first column, set the max column to 0.
+ if (maxLongitude + 180 < delta) {
+ col = 0;
+ }
+
+ return col;
+};
+
+/**
+ * Computes a sector spanned by a tile with the specified level number, row and column.
+ * @param {Level} level The tile's level number.
+ * @param {Number} row The tile's row number.
+ * @param {Number} column The tile's column number.
+ * @returns {Sector} The sector spanned by the tile.
+ * @throws {ArgumentError} If the specified level is null or undefined or the row or column are less than zero.
+ */
+Tile.computeSector = function (level, row, column) {
+ if (!level) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "computeSector", "missingLevel"));
+ }
+
+ if (row < 0 || column < 0) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "computeSector",
+ "The specified row or column is less than zero."));
+ }
+
+ var deltaLat = level.tileDelta.latitude,
+ deltaLon = level.tileDelta.longitude,
+
+ maxLat = 180 - row * deltaLat,
+ minLon = -180 + column * deltaLon,
+ minLat = maxLat - deltaLat,
+ maxLon = minLon + deltaLon;
+
+ return new Sector(minLat, maxLat, minLon, maxLon);
+};
+
+/**
+ * Creates all tiles for a specified level number.
+ * @param {Level} level The level to create the tiles for.
+ * @param {TileFactory} tileFactory The tile factory to use for creating tiles.
+ * @param {Tile[]} result An array in which to return the results.
+ * @throws {ArgumentError} If any argument is null or undefined.
+ */
+Tile.createTilesForLevel = function (level, tileFactory, result) {
+ if (!level) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "createTilesForLevel", "missingLevel"));
+ }
+
+ if (!tileFactory) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "createTilesForLevel",
+ "The specified tile factory is null or undefined"));
+ }
+
+ if (!result) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "createTilesForLevel", "missingResult"));
+ }
+
+ var deltaLat = level.tileDelta.latitude,
+ deltaLon = level.tileDelta.longitude,
+
+ sector = level.sector,
+ firstRow = Tile.computeRow(deltaLat, sector.maxLatitude),
+ lastRow = Tile.computeRow(deltaLat, sector.minLatitude),
+
+ firstCol = Tile.computeColumn(deltaLon, sector.minLongitude),
+ lastCol = Tile.computeColumn(deltaLon, sector.maxLongitude),
+
+ firstRowLat = 180 - firstRow * deltaLat,
+ firstRowLon = -180 + firstCol * deltaLon,
+
+ minLat,
+ minLon,
+ maxLat = firstRowLat,
+ maxLon;
+
+ for (var row = firstRow; row <= lastRow; row += 1) {
+ minLat = maxLat - deltaLat;
+ minLon = firstRowLon;
+
+ for (var col = firstCol; col <= lastCol; col += 1) {
+ maxLon = minLon + deltaLon;
+ var tileSector = new Sector(minLat, maxLat, minLon, maxLon),
+ tile = tileFactory.createTile(tileSector, level, row, col);
+ result.push(tile);
+
+ minLon = maxLon;
+ }
+
+ maxLat = minLat;
+ }
+};
+
+export default Tile;
diff --git a/web/test/WebWorldWind/src/util/TileFactory.js b/web/test/WebWorldWind/src/util/TileFactory.js
new file mode 100644
index 00000000..d200a97c
--- /dev/null
+++ b/web/test/WebWorldWind/src/util/TileFactory.js
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports TileFactory
+ */
+import Logger from '../util/Logger';
+import UnsupportedOperationError from '../error/UnsupportedOperationError';
+
+
+/**
+ * Applications must not call this constructor. It is an interface class and is not meant to be instantiated
+ * directly.
+ * @alias TileFactory
+ * @constructor
+ * @classdesc
+ * Represents a tile factory.
+ * This is an interface class and is not meant to be instantiated directly.
+ */
+function TileFactory() {
+}
+
+/**
+ * Creates a tile for a specified sector, level and row and column within that level.
+ * Implementers of this interface must implement this function.
+ * @param {Sector} sector The sector the tile spans.
+ * @param {Level} level The level the tile is a member of.
+ * @param {Number} row The tile's row within the specified level.
+ * @param {Number} column The tile's column within the specified level.
+ * @throws {ArgumentError} If the specified sector is null or undefined.
+ */
+TileFactory.prototype.createTile = function (sector, level, row, column) {
+ throw new UnsupportedOperationError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "TileFactory", "createTile", "abstractInvocation"));
+};
+
+export default TileFactory;
diff --git a/web/test/WebWorldWind/src/util/UrlBuilder.js b/web/test/WebWorldWind/src/util/UrlBuilder.js
new file mode 100644
index 00000000..5f93ab14
--- /dev/null
+++ b/web/test/WebWorldWind/src/util/UrlBuilder.js
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports UrlBuilder
+ */
+import Logger from '../util/Logger';
+import UnsupportedOperationError from '../error/UnsupportedOperationError';
+
+
+/**
+ * Applications must not call this constructor. It is an interface class and is not meant to be instantiated.
+ * @alias UrlBuilder
+ * @constructor
+ * @classdesc
+ * Defines an interface for tile URL builders. This is an interface class and not meant to be instantiated.
+ */
+function UrlBuilder() {
+}
+
+/**
+ * Creates the URL string for a resource.
+ * @param {Tile} tile The tile for which to create the URL.
+ * @param {String} format The format to request.
+ * @returns {String} A string identifying the URL for the specified tile's resource.
+ * @throws {ArgumentError} If either the specified tile or format is null or undefined.
+ */
+UrlBuilder.prototype.urlForTile = function (tile, format) {
+ throw new UnsupportedOperationError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "UrlBuilder", "urlForTile", "abstractInvocation"));
+};
+
+export default UrlBuilder;
diff --git a/web/test/WebWorldWind/src/util/WWMath.js b/web/test/WebWorldWind/src/util/WWMath.js
new file mode 100644
index 00000000..e6245f7b
--- /dev/null
+++ b/web/test/WebWorldWind/src/util/WWMath.js
@@ -0,0 +1,873 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import Angle from '../geom/Angle';
+import ArgumentError from '../error/ArgumentError';
+import Logger from '../util/Logger';
+import Rectangle from '../geom/Rectangle';
+import Vec3 from '../geom/Vec3';
+
+/**
+ * Provides math constants and functions.
+ * @exports WWMath
+ */
+var WWMath = {
+
+ /**
+ * Returns a number within the range of a specified minimum and maximum.
+ * @param {Number} value The value to clamp.
+ * @param {Number} minimum The minimum value to return.
+ * @param {Number} maximum The maximum value to return.
+ * @returns {Number} The minimum value if the specified value is less than the minimum, the maximum value if
+ * the specified value is greater than the maximum, otherwise the value specified is returned.
+ */
+ clamp: function (value, minimum, maximum) {
+ return value < minimum ? minimum : value > maximum ? maximum : value;
+ },
+
+ /**
+ * Computes a number between two numbers.
+ * @param amount {Number} The relative distance between the numbers at which to compute the new number. This
+ * should normally be a number between 0 and 1 but whatever number is specified is applied.
+ * @param {Number} value1 The first number.
+ * @param {Number} value2 The second number.
+ * @returns {Number} the computed value.
+ */
+ interpolate: function (amount, value1, value2) {
+ return (1 - amount) * value1 + amount * value2;
+ },
+
+ /**
+ * Returns the cube root of a specified value.
+ * @param {Number} x The value whose cube root is computed.
+ * @returns {Number} The cube root of the specified number.
+ */
+ cbrt: function (x) {
+ // Use the built-in version if it exists. cbrt() is defined in ECMA6.
+ if (typeof Math.cbrt === 'function') {
+ return Math.cbrt(x);
+ } else {
+ return Math.pow(x, 1 / 3);
+ }
+ },
+
+ /**
+ * Computes the Cartesian intersection point of a specified line with an ellipsoid.
+ * @param {Line} line The line for which to compute the intersection.
+ * @param {Number} equatorialRadius The ellipsoid's major radius.
+ * @param {Number} polarRadius The ellipsoid's minor radius.
+ * @param {Vec3} result A pre-allocated Vec3 instance in which to return the computed point.
+ * @returns {boolean} true if the line intersects the ellipsoid, otherwise false
+ * @throws {ArgumentError} If the specified line or result is null or undefined.
+ * @deprecated utilize the Globe.intersectsLine method attached implementation
+ */
+ computeEllipsoidalGlobeIntersection: function (line, equatorialRadius, polarRadius, result) {
+ if (!line) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WWMath",
+ "computeEllipsoidalGlobeIntersection", "missingLine"));
+ }
+
+ if (!result) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WWMath",
+ "computeEllipsoidalGlobeIntersection", "missingResult"));
+ }
+
+ // Taken from "Mathematics for 3D Game Programming and Computer Graphics, Second Edition", Section 5.2.3.
+ //
+ // Note that the parameter n from in equations 5.70 and 5.71 is omitted here. For an ellipsoidal globe this
+ // parameter is always 1, so its square and its product with any other value simplifies to the identity.
+
+ var vx = line.direction[0],
+ vy = line.direction[1],
+ vz = line.direction[2],
+ sx = line.origin[0],
+ sy = line.origin[1],
+ sz = line.origin[2],
+ m = equatorialRadius / polarRadius, // ratio of the x semi-axis length to the y semi-axis length
+ m2 = m * m,
+ r2 = equatorialRadius * equatorialRadius, // nominal radius squared
+ a = vx * vx + m2 * vy * vy + vz * vz,
+ b = 2 * (sx * vx + m2 * sy * vy + sz * vz),
+ c = sx * sx + m2 * sy * sy + sz * sz - r2,
+ d = b * b - 4 * a * c, // discriminant
+ t;
+
+ if (d < 0) {
+ return false;
+ }
+ else {
+ t = (-b - Math.sqrt(d)) / (2 * a);
+ result[0] = sx + vx * t;
+ result[1] = sy + vy * t;
+ result[2] = sz + vz * t;
+ return true;
+ }
+ },
+
+ /**
+ * Computes the Cartesian intersection point of a specified line with a triangle.
+ * @param {Line} line The line for which to compute the intersection.
+ * @param {Vec3} vertex0 The triangle's first vertex.
+ * @param {Vec3} vertex1 The triangle's second vertex.
+ * @param {Vec3} vertex2 The triangle's third vertex.
+ * @param {Vec3} result A pre-allocated Vec3 instance in which to return the computed point.
+ * @returns {boolean} true if the line intersects the triangle, otherwise false
+ * @throws {ArgumentError} If the specified line, vertex or result is null or undefined.
+ */
+ computeTriangleIntersection: function (line, vertex0, vertex1, vertex2, result) {
+ if (!line) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WWMath",
+ "computeTriangleIntersection", "missingLine"));
+ }
+
+ if (!vertex0 || !vertex1 || !vertex2) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WWMath",
+ "computeTriangleIntersection", "missingVertex"));
+ }
+
+ if (!result) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WWMath",
+ "computeTriangleIntersection", "missingResult"));
+ }
+
+ // Taken from Moller and Trumbore
+ // https://www.cs.virginia.edu/~gfx/Courses/2003/ImageSynthesis/papers/Acceleration/Fast%20MinimumStorage%20RayTriangle%20Intersection.pdf
+
+ var vx = line.direction[0],
+ vy = line.direction[1],
+ vz = line.direction[2],
+ sx = line.origin[0],
+ sy = line.origin[1],
+ sz = line.origin[2],
+ EPSILON = 0.00001;
+
+ // find vectors for two edges sharing point a: vertex1 - vertex0 and vertex2 - vertex0
+ var edge1x = vertex1[0] - vertex0[0],
+ edge1y = vertex1[1] - vertex0[1],
+ edge1z = vertex1[2] - vertex0[2],
+ edge2x = vertex2[0] - vertex0[0],
+ edge2y = vertex2[1] - vertex0[1],
+ edge2z = vertex2[2] - vertex0[2];
+
+ // Compute cross product of line direction and edge2
+ var px = vy * edge2z - vz * edge2y,
+ py = vz * edge2x - vx * edge2z,
+ pz = vx * edge2y - vy * edge2x;
+
+ // Get determinant
+ var det = edge1x * px + edge1y * py + edge1z * pz; // edge1 dot p
+ if (det > -EPSILON && det < EPSILON) { // if det is near zero then ray lies in plane of triangle
+ return false;
+ }
+
+ var inv_det = 1.0 / det;
+
+ // Compute distance for vertex A to ray origin: origin - vertex0
+ var tx = sx - vertex0[0],
+ ty = sy - vertex0[1],
+ tz = sz - vertex0[2];
+
+ // Calculate u parameter and test bounds: 1/det * t dot p
+ var u = inv_det * (tx * px + ty * py + tz * pz);
+ if (u < -EPSILON || u > 1 + EPSILON) {
+ return false;
+ }
+
+ // Prepare to test v parameter: t cross edge1
+ var qx = ty * edge1z - tz * edge1y,
+ qy = tz * edge1x - tx * edge1z,
+ qz = tx * edge1y - ty * edge1x;
+
+ // Calculate v parameter and test bounds: 1/det * dir dot q
+ var v = inv_det * (vx * qx + vy * qy + vz * qz);
+ if (v < -EPSILON || u + v > 1 + EPSILON) {
+ return false;
+ }
+
+ // Calculate the point of intersection on the line: t = 1/det * edge2 dot q
+ var t = inv_det * (edge2x * qx + edge2y * qy + edge2z * qz);
+ if (t < 0) {
+ return false;
+ } else {
+ result[0] = sx + vx * t;
+ result[1] = sy + vy * t;
+ result[2] = sz + vz * t;
+ return true;
+ }
+ },
+
+ computeIndexedTrianglesIntersection: function (line, points, indices, results) {
+ if (!line) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WWMath",
+ "computeIndexedTrianglesIntersection", "missingLine"));
+ }
+
+ if (!points) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WWMath",
+ "computeIndexedTrianglesIntersection", "missingPoints"));
+ }
+
+ if (!indices) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WWMath",
+ "computeIndexedTrianglesIntersection", "missingIndices"));
+ }
+
+ if (!results) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WWMath",
+ "computeIndexedTrianglesIntersection", "missingResults"));
+ }
+
+ var v0 = new Vec3(0, 0, 0),
+ v1 = new Vec3(0, 0, 0),
+ v2 = new Vec3(0, 0, 0),
+ iPoint = new Vec3(0, 0, 0);
+
+ for (var i = 0, len = indices.length; i < len; i += 3) {
+ var i0 = 3 * indices[i],
+ i1 = 3 * indices[i + 1],
+ i2 = 3 * indices[i + 2];
+
+ v0[0] = points[i0];
+ v0[1] = points[i0 + 1];
+ v0[2] = points[i0 + 2];
+
+ v1[0] = points[i1];
+ v1[1] = points[i1 + 1];
+ v1[2] = points[i1 + 2];
+
+ v2[0] = points[i2];
+ v2[1] = points[i2 + 1];
+ v2[2] = points[i2 + 2];
+
+ if (WWMath.computeTriangleIntersection(line, v0, v1, v2, iPoint)) {
+ results.push(iPoint);
+ iPoint = new Vec3(0, 0, 0);
+ }
+ }
+
+ return results.length > 0;
+ },
+
+ /**
+ * Computes the Cartesian intersection points of a specified line with a triangle strip. The triangle strip
+ * is specified by a list of vertex points and a list of indices indicating the triangle strip tessellation
+ * of those vertices. The triangle strip indices are interpreted in the same manner as WebGL, where each
+ * index indicates a vertex position rather than an actual index into the points array (e.g. a triangle
+ * strip index of 1 indicates the XYZ tuple starting at array index 3). This is equivalent to calling
+ * computeTriangleIntersection for each individual triangle in the triangle strip, but is potentially much
+ * more efficient.
+ * @param {Line} line The line for which to compute the intersection.
+ * @param {Array} points The list of vertex points, organized as a list of tightly-packed XYZ tuples.
+ * @param {Array} indices The list of triangle strip indices, organized as a list of vertex positions.
+ * @param {Array} results A pre-allocated array instance in which to return the intersection points as
+ * {@link Vec3} instances.
+ * @throws {ArgumentError} If the specified line, points, indices or results is null or undefined.
+ */
+ computeTriStripIntersections: function (line, points, indices, results) {
+ if (!line) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WWMath",
+ "computeTriStripIntersections", "missingLine"));
+ }
+
+ if (!points) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WWMath",
+ "computeTriStripIntersections", "missingPoints"));
+ }
+
+ if (!indices) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WWMath",
+ "computeTriStripIntersections", "missingIndices"));
+ }
+
+ if (!results) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WWMath",
+ "computeTriStripIntersections", "missingResults"));
+ }
+
+ // Taken from Moller and Trumbore
+ // https://www.cs.virginia.edu/~gfx/Courses/2003/ImageSynthesis/papers/Acceleration/Fast%20MinimumStorage%20RayTriangle%20Intersection.pdf
+
+ // Adapted from the original ray-triangle intersection algorithm to optimize for ray-triangle strip
+ // intersection. We optimize by reusing constant terms, replacing use of Vec3 with inline primitives,
+ // and exploiting the triangle strip organization to reuse computations common to adjacent triangles.
+ // These optimizations reduce worst-case terrain picking performance by approximately 50% in Chrome on a
+ // 2010 iMac and a Nexus 9.
+
+ var vx = line.direction[0],
+ vy = line.direction[1],
+ vz = line.direction[2],
+ sx = line.origin[0],
+ sy = line.origin[1],
+ sz = line.origin[2],
+ vert0x, vert0y, vert0z,
+ vert1x, vert1y, vert1z,
+ vert2x, vert2y, vert2z,
+ edge1x, edge1y, edge1z,
+ edge2x, edge2y, edge2z,
+ px, py, pz,
+ tx, ty, tz,
+ qx, qy, qz,
+ u, v, t,
+ det, inv_det,
+ index,
+ EPSILON = 0.00001;
+
+ // Get the triangle strip's first vertex.
+ index = 3 * indices[0];
+ vert1x = points[index++];
+ vert1y = points[index++];
+ vert1z = points[index];
+
+ // Get the triangle strip's second vertex.
+ index = 3 * indices[1];
+ vert2x = points[index++];
+ vert2y = points[index++];
+ vert2z = points[index];
+
+ // Compute the intersection of each triangle with the specified ray.
+ for (var i = 2, len = indices.length; i < len; i++) {
+ // Move the last two vertices into the first two vertices. This takes advantage of the triangle
+ // strip's structure and avoids redundant reads from points and indices. During the first
+ // iteration this places the triangle strip's first three vertices in vert0, vert1 and vert2,
+ // respectively.
+ vert0x = vert1x;
+ vert0y = vert1y;
+ vert0z = vert1z;
+ vert1x = vert2x;
+ vert1y = vert2y;
+ vert1z = vert2z;
+
+ // Get the triangle strip's next vertex.
+ index = 3 * indices[i];
+ vert2x = points[index++];
+ vert2y = points[index++];
+ vert2z = points[index];
+
+ // find vectors for two edges sharing point a: vert1 - vert0 and vert2 - vert0
+ edge1x = vert1x - vert0x;
+ edge1y = vert1y - vert0y;
+ edge1z = vert1z - vert0z;
+ edge2x = vert2x - vert0x;
+ edge2y = vert2y - vert0y;
+ edge2z = vert2z - vert0z;
+
+ // Compute cross product of line direction and edge2
+ px = vy * edge2z - vz * edge2y;
+ py = vz * edge2x - vx * edge2z;
+ pz = vx * edge2y - vy * edge2x;
+
+ // Get determinant
+ det = edge1x * px + edge1y * py + edge1z * pz; // edge1 dot p
+ if (det > -EPSILON && det < EPSILON) { // if det is near zero then ray lies in plane of triangle
+ continue;
+ }
+
+ inv_det = 1.0 / det;
+
+ // Compute distance for vertex A to ray origin: origin - vert0
+ tx = sx - vert0x;
+ ty = sy - vert0y;
+ tz = sz - vert0z;
+
+ // Calculate u parameter and test bounds: 1/det * t dot p
+ u = inv_det * (tx * px + ty * py + tz * pz);
+ if (u < -EPSILON || u > 1 + EPSILON) {
+ continue;
+ }
+
+ // Prepare to test v parameter: tvec cross edge1
+ qx = ty * edge1z - tz * edge1y;
+ qy = tz * edge1x - tx * edge1z;
+ qz = tx * edge1y - ty * edge1x;
+
+ // Calculate v parameter and test bounds: 1/det * dir dot q
+ v = inv_det * (vx * qx + vy * qy + vz * qz);
+ if (v < -EPSILON || u + v > 1 + EPSILON) {
+ continue;
+ }
+
+ // Calculate the point of intersection on the line: t = 1/det * edge2 dot q
+ t = inv_det * (edge2x * qx + edge2y * qy + edge2z * qz);
+ if (t >= 0) {
+ results.push(new Vec3(sx + vx * t, sy + vy * t, sz + vz * t));
+ }
+ }
+ },
+
+ /**
+ * Computes the absolute value of a specified value.
+ * @param {Number} a The value whose absolute value to compute.
+ * @returns {Number} The absolute value of the specified number.
+ */
+ fabs: function (a) {
+ return a >= 0 ? a : -a;
+ },
+
+ /**
+ * Computes the floating-point modulus of a specified number.
+ * @param {Number} number The number whose modulus to compute.
+ * @param {Number} modulus The modulus.
+ * @returns {Number} The remainder after dividing the number by the modulus: number % modulus.
+ */
+ fmod: function (number, modulus) {
+ return modulus === 0 ? 0 : number - Math.floor(number / modulus) * modulus;
+ },
+
+ /**
+ * Returns the fractional part of a specified number
+ * @param {Number} number The number whose fractional part to compute.
+ * @returns {Number} The fractional part of the specified number: number - floor(number).
+ */
+ fract: function (number) {
+ return number - Math.floor(number);
+ },
+
+ /**
+ * Returns the integer modulus of a specified number. This differs from the % operator in that
+ * the result is always positive when the modulus is positive. For example -1 % 10 = -1,
+ * whereas mod(-1, 10) = 1.
+ * @param {Number} number The number whose modulus to compute.
+ * @param {Number} modulus The modulus.
+ * @returns {Number} The remainder after dividing the number by the modulus.
+ */
+ mod: function (number, modulus) {
+ return (number % modulus + modulus) % modulus;
+ },
+
+ /**
+ * Returns the maximum of two specified numbers.
+ * @param {Number} value1 The first value to compare.
+ * @param {Number} value2 The second value to compare.
+ * @returns {Number} The maximum of the two specified values.
+ */
+ max: function (value1, value2) {
+ return value1 > value2 ? value1 : value2;
+ },
+
+ /**
+ * Computes the axes of a local coordinate system on the specified globe, placing the resultant axes in the specified
+ * axis arguments.
+ *
+ * Upon return the specified axis arguments contain three orthogonal axes identifying the X, Y, and Z axes. Each
+ * axis has unit length.
+ *
+ * The local coordinate system is defined such that the Z axis maps to the globe's surface normal at the point, the
+ * Y axis maps to the north pointing tangent, and the X axis maps to the east pointing tangent.
+ *
+ * @param {Vec3} origin The local coordinate system origin, in model coordinates.
+ * @param {Globe} globe The globe the coordinate system is relative to.
+ * @param {Vec3} xAxisResult A pre-allocated Vec3 in which to return the computed X axis.
+ * @param {Vec3} yAxisResult A pre-allocated Vec3 in which to return the computed Y axis.
+ * @param {Vec3} zAxisResult A pre-allocated Vec3 in which to return the computed Z axis.
+ * @throws {ArgumentError} If any argument is null or undefined.
+ */
+ localCoordinateAxesAtPoint: function (origin, globe, xAxisResult, yAxisResult, zAxisResult) {
+ if (!origin) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WWMath",
+ "localCoordinateAxesAtPoint", "missingVector"));
+ }
+
+ if (!globe) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WWMath",
+ "localCoordinateAxesAtPoint", "missingGlobe"));
+ }
+
+ if (!xAxisResult || !yAxisResult || !zAxisResult) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WWMath",
+ "localCoordinateAxesAtPoint", "missingResult"));
+ }
+
+ var x = origin[0],
+ y = origin[1],
+ z = origin[2];
+
+ // Compute the z axis from the surface normal in model coordinates. This axis is used to determine the other two
+ // axes, and is the only constant in the computations below.
+ globe.surfaceNormalAtPoint(x, y, z, zAxisResult);
+
+ // Compute the y axis from the north pointing tangent in model coordinates. This axis is known to be orthogonal to
+ // the z axis, and is therefore used to compute the x axis.
+ globe.northTangentAtPoint(x, y, z, yAxisResult);
+
+ // Compute the x axis as the cross product of the y and z axes. This ensures that the x and z axes are orthogonal.
+ xAxisResult.set(yAxisResult[0], yAxisResult[1], yAxisResult[2]);
+ xAxisResult.cross(zAxisResult);
+ xAxisResult.normalize();
+
+ // Re-compute the y axis as the cross product of the z and x axes. This ensures that all three axes are orthogonal.
+ // Though the initial y axis computed above is likely to be very nearly orthogonal, we re-compute it using cross
+ // products to reduce the effect of floating point rounding errors caused by working with Earth sized coordinates.
+ yAxisResult.set(zAxisResult[0], zAxisResult[1], zAxisResult[2]);
+ yAxisResult.cross(xAxisResult);
+ yAxisResult.normalize();
+ },
+
+ /**
+ * Computes the distance to a globe's horizon from a viewer at a given altitude.
+ *
+ * Only the globe's ellipsoid is considered; terrain height is not incorporated. This returns zero if the radius is zero
+ * or if the altitude is less than or equal to zero.
+ *
+ * @param {Number} radius The globe's radius, in meters.
+ * @param {Number} altitude The viewer's altitude above the globe, in meters.
+ * @returns {Number} The distance to the horizon, in model coordinates.
+ * @throws {ArgumentError} If the specified globe radius is negative.
+ */
+ horizonDistanceForGlobeRadius: function (radius, altitude) {
+ if (radius < 0) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WWMath",
+ "horizontalDistanceForGlobeRadius", "The specified globe radius is negative."));
+ }
+
+ return radius > 0 && altitude > 0 ? Math.sqrt(altitude * (2 * radius + altitude)) : 0;
+ },
+
+ /**
+ * Computes the near clip distance that corresponds to a specified far clip distance and resolution at the far clip
+ * plane.
+ *
+ * This computes a near clip distance appropriate for use in [perspectiveFrustumRect]{@link WWMath#perspectiveFrustumRectangle}
+ * and [setToPerspectiveProjection]{@link Matrix#setToPerspectiveProjection}. This returns zero if either the distance or the
+ * resolution are zero.
+ *
+ * @param {Number} farDistance The far clip distance, in meters.
+ * @param {Number} farResolution The depth resolution at the far clip plane, in meters.
+ * @param {Number} depthBits The number of bit-planes in the depth buffer.
+ * @returns {Number} The near clip distance, in meters.
+ * @throws {ArgumentError} If either the distance or resolution is negative, or if the depth bits is less
+ * than one.
+ */
+ perspectiveNearDistanceForFarDistance: function (farDistance, farResolution, depthBits) {
+ if (farDistance < 0) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WWMath", "perspectiveNearDistanceForFarDistance",
+ "The specified distance is negative."));
+ }
+
+ if (farResolution < 0) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WWMath", "perspectiveNearDistanceForFarDistance",
+ "The specified resolution is negative."));
+ }
+
+ if (depthBits < 1) {
+ throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WWMath", "perspectiveNearDistanceForFarDistance",
+ "The specified depth bits is negative."));
+ }
+
+ var maxDepthValue = (1 << depthBits) - 1;
+
+ return farDistance / (maxDepthValue / (1 - farResolution / farDistance) - maxDepthValue + 1);
+ },
+
+ /**
+ * Computes the maximum near clip distance for a perspective projection that avoids clipping an object at a
+ * given distance from the eye point.
+ * true if the specified value is a power of two,
+ * otherwise false.
+ */
+ isPowerOfTwo: function (value) {
+ return value != 0 && (value & value - 1) === 0;
+ },
+
+ /**
+ * Determine the sign of a number.
+ * @param {Number} value The value to determine the sign of.
+ * @returns {Number} 1, -1, or 0, depending on the sign of the value.
+ */
+ signum: function (value) {
+ return value > 0 ? 1 : value < 0 ? -1 : 0;
+ },
+
+ /**
+ * Calculates the Gudermannian inverse used to unproject Mercator projections.
+ * @param {Number} latitude The latitude in degrees.
+ * @returns {Number} The Gudermannian inverse for the specified latitude.
+ */
+ gudermannianInverse: function (latitude) {
+ return Math.log(Math.tan(Math.PI / 4 + latitude * Angle.DEGREES_TO_RADIANS / 2)) / Math.PI;
+ },
+
+ epsg3857ToEpsg4326: function (easting, northing) {
+ var r = 6.3781e6,
+ latRadians = Math.PI / 2 - 2 * Math.atan(Math.exp(-northing / r)),
+ lonRadians = easting / r;
+
+ return [
+ WWMath.clamp(latRadians * Angle.RADIANS_TO_DEGREES, -90, 90),
+ WWMath.clamp(lonRadians * Angle.RADIANS_TO_DEGREES, -180, 180)
+ ];
+ },
+
+ /**
+ * Returns the value that is the nearest power of 2 less than or equal to the given value.
+ * @param {Number} value the reference value. The power of 2 returned is less than or equal to this value.
+ * @returns {Number} the value that is the nearest power of 2 less than or equal to the reference value
+ */
+ powerOfTwoFloor: function (value) {
+ var power = Math.floor(Math.log(value) / Math.log(2));
+ return Math.pow(2, power);
+ },
+
+ /**
+ * Restricts an angle to the range [0, 360] degrees, wrapping angles outside the range.
+ * Wrapping takes place as though traversing the edge of a unit circle;
+ * angles less than 0 wrap back to 360, while angles greater than 360 wrap back to 0.
+ *
+ * @param {Number} degrees the angle to wrap in degrees
+ *
+ * @return {Number} the specified angle wrapped to [0, 360] degrees
+ */
+ normalizeAngle360: function (degrees) {
+ var angle = degrees % 360;
+ return angle >= 0 ? angle : angle < 0 ? 360 + angle : 360 - angle;
+ },
+
+ /**
+ * 墨卡托投影(弧度)
+ * @param {Number} lat 纬度(弧度)
+ * @returns {Number} 投影纬度(弧度)
+ * @see https://github.com/d3/d3-geo/blob/master/src/projection/mercator.js
+ */
+ _mercatorLat: function (lat) {
+ return Math.log(Math.tan((Math.PI / 2 + lat) / 2));
+ },
+
+ /**
+ * 墨卡托投影(角度)
+ * @param {Number} lat 纬度(角度)
+ * @returns {Number} 投影纬度(角度)
+ * @see https://github.com/d3/d3-geo/blob/master/src/projection/mercator.js
+ */
+ mercatorLat: function (lat) {
+ return this._mercatorLat(lat * Angle.DEGREES_TO_RADIANS) * Angle.RADIANS_TO_DEGREES;
+ },
+
+ /**
+ * 墨卡托投影反算(弧度)
+ * @param {Number} y 墨卡托投影Y坐标
+ * @returns {Number} 纬度(弧度)
+ * @see https://github.com/d3/d3-geo/blob/master/src/projection/mercator.js
+ */
+ _mercatorLatInvert: function (y) {
+ return 2 * Math.atan(Math.exp(y)) - Math.PI / 2;
+ },
+
+ /**
+ * 墨卡托投影反算(角度)
+ * @param {Number} y 墨卡托投影Y坐标(角度)
+ * @returns {Number} 纬度(角度)
+ * @see https://github.com/d3/d3-geo/blob/master/src/projection/mercator.js
+ */
+ mercatorLatInvert: function (y) {
+ return this._mercatorLatInvert(y * Angle.DEGREES_TO_RADIANS) * Angle.RADIANS_TO_DEGREES;
+ },
+
+ /**
+ * 计算两个经纬度之间距离(弧度)
+ * @param {Number} lon1 经度1(弧度)
+ * @param {Number} lat1 纬度1(弧度)
+ * @param {Number} lon2 经度2(弧度)
+ * @param {Number} lat2 纬度2(弧度)
+ * @returns {Number} 距离(米)
+ * @see https://www.xuebuyuan.com/2173606.html
+ */
+ _getDistance: function (lon1, lat1, lon2, lat2) {
+ return 2 * 6378137 * Math.asin(Math.sqrt(Math.pow(Math.sin((lat1 - lat2) / 2), 2) +
+ Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin((lon1 - lon2) / 2), 2)));
+ },
+
+ /**
+ * 计算两个经纬度之间距离(角度)
+ * @param {*} lon1 经度1(角度)
+ * @param {*} lat1 纬度1(角度)
+ * @param {*} lon2 经度2(角度)
+ * @param {*} lat2 纬度2(角度)
+ * @returns {Number} 距离(米)
+ * @see https://www.xuebuyuan.com/2173606.html
+ */
+ getDistance: function (lon1, lat1, lon2, lat2) {
+ lon1 *= Angle.DEGREES_TO_RADIANS;
+ lat1 *= Angle.DEGREES_TO_RADIANS;
+ lon2 *= Angle.DEGREES_TO_RADIANS;
+ lat2 *= Angle.DEGREES_TO_RADIANS;
+
+ return this._getDistance(lon1, lat1, lon2, lat2);
+ },
+ MAX_LAT: 180 // this.mercatorLatInvert(180) = 85.0511287798066
+};
+
+export default WWMath;
diff --git a/web/test/WebWorldWind/src/util/WWUtil.js b/web/test/WebWorldWind/src/util/WWUtil.js
new file mode 100644
index 00000000..8abc7c20
--- /dev/null
+++ b/web/test/WebWorldWind/src/util/WWUtil.js
@@ -0,0 +1,303 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import ArgumentError from '../error/ArgumentError';
+import Line from '../geom/Line';
+import Logger from '../util/Logger';
+import Rectangle from '../geom/Rectangle';
+import Vec3 from '../geom/Vec3';
+
+/**
+ * Provides math constants and functions.
+ * @exports WWUtil
+ */
+var WWUtil = {
+ // A regular expression that matches latitude followed by a comma and possible white space followed by
+ // longitude. Latitude and longitude ranges are not considered.
+ latLonRegex: /^(\-?\d+(\.\d+)?),\s*(\-?\d+(\.\d+)?)$/,
+
+ /**
+ * Returns the suffix for a specified mime type.
+ * @param {String} mimeType The mime type to determine a suffix for.
+ * @returns {String} The suffix for the specified mime type, or null if the mime type is not recognized.
+ */
+ suffixForMimeType: function (mimeType) {
+ if (mimeType === "image/png")
+ return "png";
+
+ if (mimeType === "image/jpeg")
+ return "jpg";
+
+ if (mimeType === "application/bil16")
+ return "bil";
+
+ if (mimeType === "application/bil32")
+ return "bil";
+
+ return null;
+ },
+
+ /**
+ * Returns the current location URL as obtained from window.location with the last path component
+ * removed.
+ * @returns {String} The current location URL with the last path component removed.
+ */
+ currentUrlSansFilePart: function () {
+ var protocol = window.location.protocol,
+ host = window.location.host,
+ path = window.location.pathname,
+ pathParts = path.split("/"),
+ newPath = "";
+
+ for (var i = 0, len = pathParts.length; i < len - 1; i++) {
+ if (pathParts[i].length > 0) {
+ newPath = newPath + "/" + pathParts[i];
+ }
+ }
+
+ return protocol + "//" + host + newPath;
+ },
+
+ /**
+ * Returns the URL of the directory containing the WorldWind library.
+ * @returns {String} The URL of the directory containing the WorldWind library, or null if that directory
+ * cannot be determined.
+ */
+ worldwindlibLocation: function () {
+ var scripts = document.getElementsByTagName("script"),
+ libraryName = "/worldwind.";
+
+ for (var i = 0; i < scripts.length; i++) {
+ var index = scripts[i].src.indexOf(libraryName);
+ if (index >= 0) {
+ return scripts[i].src.substring(0, index) + "/";
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Returns the path component of a specified URL.
+ * @param {String} url The URL from which to determine the path component.
+ * @returns {String} The path component, or the empty string if the specified URL is null, undefined
+ * or empty.
+ */
+ urlPath: function (url) {
+ if (!url)
+ return "";
+
+ var urlParts = url.split("/"),
+ newPath = "";
+
+ for (var i = 0, len = urlParts.length; i < len; i++) {
+ var part = urlParts[i];
+
+ if (!part || part.length === 0
+ || part.indexOf(":") != -1
+ || part === "."
+ || part === ".."
+ || part === "null"
+ || part === "undefined") {
+ continue;
+ }
+
+ if (newPath.length !== 0) {
+ newPath = newPath + "/";
+ }
+
+ newPath = newPath + part;
+ }
+
+ return newPath;
+ },
+
+ /**
+ * Sets each element of an array to a specified value. This function is intentionally generic, and works
+ * with any data structure with a length property whose elements may be referenced using array index syntax.
+ * @param array The array to fill.
+ * @param {*} value The value to assign to each array element.
+ */
+ fillArray: function (array, value) {
+ if (!array) {
+ return;
+ }
+
+ for (var i = 0, len = array.length; i < len; i++) {
+ array[i] = value;
+ }
+ },
+
+ /**
+ * Multiplies each element of an array by a specified value and assigns each element to the result. This
+ * function is intentionally generic, and works with any data structure with a length property whose
+ * elements may be referenced using array index syntax.
+ * @param array The array to fill.
+ * @param {*} value The value to multiply by each array element.
+ */
+ multiplyArray: function (array, value) {
+ if (!array) {
+ return;
+ }
+
+ for (var i = 0, len = array.length; i < len; i++) {
+ array[i] *= value;
+ }
+ },
+
+ // Used to form unique function names for JSONP callback functions.
+ jsonpCounter: 0,
+
+ /**
+ * Request a resource using JSONP.
+ * @param {String} url The url to receive the request.
+ * @param {String} parameterName The JSONP callback function key required by the server. Typically
+ * "jsonp" or "callback".
+ * @param {Function} callback The function to invoke when the request succeeds. The function receives
+ * one argument, the JSON payload of the JSONP request.
+ */
+ jsonp: function (url, parameterName, callback) {
+
+ // Generate a unique function name for the JSONP callback.
+ var functionName = "gov_nasa_worldwind_jsonp_" + WWUtil.jsonpCounter++;
+
+ // Define a JSONP callback function. Assign it to global scope the browser can find it.
+ window[functionName] = function (jsonData) {
+ // Remove the JSONP callback from global scope.
+ delete window[functionName];
+
+ // Call the client's callback function.
+ callback(jsonData);
+ };
+
+ // Append the callback query parameter to the URL.
+ var jsonpUrl = url + (url.indexOf('?') === -1 ? '?' : '&');
+ jsonpUrl += parameterName + "=" + functionName;
+
+ // Create a script element for the browser to invoke.
+ var script = document.createElement('script');
+ script.async = true;
+ script.src = jsonpUrl;
+
+ // Prepare to add the script to the document's head.
+ var head = document.getElementsByTagName('head')[0];
+
+ // Set up to remove the script element once it's invoked.
+ var cleanup = function () {
+ script.onload = undefined;
+ script.onerror = undefined;
+ head.removeChild(script);
+ };
+
+ script.onload = cleanup;
+ script.onerror = cleanup;
+
+ // Add the script element to the document, causing the browser to invoke it.
+ head.appendChild(script);
+ },
+
+ arrayEquals: function (array1, array2) {
+ return (array1.length == array2.length) && array1.every(function (element, index) {
+ return element === array2[index] || element.equals && element.equals(array2[index]);
+ });
+ },
+
+ /**
+ * It transforms given item to the boolean. It respects that 0, "0" and "false" are percieved as false
+ * on top of the standard Boolean function.
+ * @param item {String} Item to transform
+ * @returns {boolean} Value transformed to the boolean.
+ */
+ transformToBoolean: function (item) {
+ if (item == 0 || item == "0" || item == "false") {
+ return false;
+ } else {
+ return Boolean(item);
+ }
+ },
+
+ /**
+ * It clones original object into the new one. It is necessary to retain the options information valid
+ * for all nodes.
+ * @param original Object to clone
+ * @returns {Object} Cloned object
+ */
+ clone: function (original) {
+ var clone = {};
+ var i, keys = Object.keys(original);
+
+ for (i = 0; i < keys.length; i++) {
+ // copy each property into the clone
+ clone[keys[i]] = original[keys[i]];
+ }
+
+ return clone;
+ },
+
+ /**
+ * It returns unique GUID.
+ * @returns {string} String representing unique identifier in the application.
+ */
+ guid: function () {
+ function s4() {
+ return Math.floor((1 + Math.random()) * 0x10000)
+ .toString(16)
+ .substring(1);
+ }
+
+ return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
+ s4() + '-' + s4() + s4() + s4();
+ },
+
+ /**
+ * Transforms item to date. It accepts ISO-8601 format.
+ * @param item {String} To transform.
+ * @returns {Date} Date extracted from the current information.
+ */
+ date: function (item) {
+ return new Date(item);
+ },
+
+ /**
+ * Determines whether subjectString begins with the characters of searchString.
+ * @param {String} subjectString The string to analyse.
+ * @param {String} searchString The characters to be searched for at the start of subjectString.
+ * @param {Number} position The position in subjectString at which to begin searching for searchString; defaults to 0.
+ * @return {Boolean} true if the given characters are found at the beginning of the string; otherwise, false.
+ */
+ startsWith: function (subjectString, searchString, position) {
+ position = position || 0;
+ return subjectString.substr(position, searchString.length) === searchString;
+ },
+
+ /**
+ * Determines whether subjectString ends with the characters of searchString.
+ * @param {String} subjectString The string to analyse.
+ * @param {String} searchString The characters to be searched for at the end of subjectString.
+ * @param {Number} length Optional. If provided overwrites the considered length of the string to search in. If omitted, the default value is the length of the string.
+ * @return {Boolean} true if the given characters are found at the end of the string; otherwise, false.
+ */
+ endsWith: function (subjectString, searchString, length) {
+ if (typeof length !== 'number' || !isFinite(length) || Math.floor(length) !== length || length > subjectString.length) {
+ length = subjectString.length;
+ }
+ length -= searchString.length;
+ var lastIndex = subjectString.lastIndexOf(searchString, length);
+ return lastIndex !== -1 && lastIndex === length;
+ }
+};
+
+export default WWUtil;
diff --git a/web/test/WebWorldWind/src/util/WmsUrlBuilder.js b/web/test/WebWorldWind/src/util/WmsUrlBuilder.js
new file mode 100644
index 00000000..e8b5062e
--- /dev/null
+++ b/web/test/WebWorldWind/src/util/WmsUrlBuilder.js
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2003-2006, 2009, 2017, United States Government, as represented by the Administrator of the
+ * National Aeronautics and Space Administration. All rights reserved.
+ *
+ * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * @exports WmsUrlBuilder
+ */
+import ArgumentError from '../error/ArgumentError';
+import Logger from '../util/Logger';
+
+
+/**
+ * Constructs a WMS URL builder.
+ * @alias WmsUrlBuilder
+ * @constructor
+ * @classdesc Provides a factory to create URLs for WMS Get Map requests.
+ * @param {String} serviceAddress The address of the WMS server.
+ * @param {String} layerNames The comma-separated list of names of the layers to retrieve.
+ * @param {String} styleNames The comma-separated list of names of the styles to retrieve. May be null.
+ * @param {String} wmsVersion The version of the WMS server. May be null, in which case version 1.3.0 is
+ * assumed.
+ * @param {String} timeString The time parameter included in GetMap requests.
+ * May be null, in which case no time parameter is included in the request.
+ * @throws {ArgumentError} If the service address or layer names are null or empty.
+ *
+ */
+function WmsUrlBuilder(serviceAddress, layerNames, styleNames, wmsVersion, timeString) {
+ if (!serviceAddress || serviceAddress.length === 0) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "WmsUrlBuilder", "constructor",
+ "The WMS service address is missing."));
+ }
+
+ if (!layerNames || layerNames.length === 0) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "WmsUrlBuilder", "constructor",
+ "The WMS layer names are not specified."));
+ }
+
+ /**
+ * The address of the WMS server.
+ * @type {String}
+ */
+ this.serviceAddress = serviceAddress;
+
+ /**
+ * The comma-separated list of layer names to retrieve.
+ * @type {String}
+ */
+ this.layerNames = layerNames;
+
+ /**
+ * The comma-separated list of style names to retrieve.
+ * @type {String}
+ */
+ this.styleNames = styleNames ? styleNames : "";
+
+ /**
+ * Indicates whether the layer should be requested with transparency.
+ * @type {Boolean}
+ * @default true
+ */
+ this.transparent = true;
+
+ /**
+ * The WMS version to specify when requesting resources.
+ * @type {String}
+ * @default 1.3.0
+ */
+ this.wmsVersion = wmsVersion && wmsVersion.length > 0 ? wmsVersion : "1.3.0";
+ this.isWms130OrGreater = this.wmsVersion >= "1.3.0";
+
+ /**
+ * The coordinate reference system to use when requesting layers.
+ * @type {String}
+ * @default EPSG:4326
+ */
+ this.crs = "EPSG:4326";
+
+ /**
+ * The time parameter included in GetMap requests. If null, no time parameter is included in the requests.
+ * @type {String}
+ */
+ this.timeString = timeString;
+}
+
+/**
+ * Creates the URL string for a WMS Get Map request.
+ * @param {Tile} tile The tile for which to create the URL.
+ * @param {String} imageFormat The image format to request.
+ * @throws {ArgumentError} If the specified tile or image format are null or undefined.
+ */
+WmsUrlBuilder.prototype.urlForTile = function (tile, imageFormat) {
+ if (!tile) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "WmsUrlBuilder", "urlForTile", "missingTile"));
+ }
+
+ if (!imageFormat) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "WmsUrlBuilder", "urlForTile",
+ "The image format is null or undefined."));
+ }
+
+ var sector = tile.sector;
+
+ var sb = WmsUrlBuilder.fixGetMapString(this.serviceAddress);
+
+ if (sb.search(/service=wms/i) < 0) {
+ sb = sb + "service=WMS";
+ }
+
+ sb = sb + "&request=GetMap";
+ sb = sb + "&version=" + this.wmsVersion;
+ sb = sb + "&transparent=" + (this.transparent ? "TRUE" : "FALSE");
+ sb = sb + "&layers=" + this.layerNames;
+ sb = sb + "&styles=" + this.styleNames;
+ sb = sb + "&format=" + imageFormat;
+ sb = sb + "&width=" + tile.tileWidth;
+ sb = sb + "&height=" + tile.tileHeight;
+
+ if (this.timeString) {
+ sb = sb + "&time=" + this.timeString;
+ }
+
+ if (this.isWms130OrGreater) {
+ sb = sb + "&crs=" + this.crs;
+ sb = sb + "&bbox=";
+ if (this.crs === "CRS:84") {
+ sb = sb + sector.minLongitude + "," + sector.minLatitude + ",";
+ sb = sb + sector.maxLongitude + "," + sector.maxLatitude;
+ } else {
+ sb = sb + sector.minLatitude + "," + sector.minLongitude + ",";
+ sb = sb + sector.maxLatitude + "," + sector.maxLongitude;
+ }
+ } else {
+ sb = sb + "&srs=" + this.crs;
+ sb = sb + "&bbox=";
+ sb = sb + sector.minLongitude + "," + sector.minLatitude + ",";
+ sb = sb + sector.maxLongitude + "," + sector.maxLatitude;
+ }
+
+ sb = sb.replace(" ", "%20");
+
+ return sb;
+};
+
+// Intentionally not documented.
+WmsUrlBuilder.fixGetMapString = function (serviceAddress) {
+ if (!serviceAddress) {
+ throw new ArgumentError(
+ Logger.logMessage(Logger.LEVEL_SEVERE, "WmsUrlBuilder", "fixGetMapString",
+ "The specified service address is null or undefined."));
+ }
+
+ var index = serviceAddress.indexOf("?");
+
+ if (index < 0) { // if string contains no question mark
+ serviceAddress = serviceAddress + "?"; // add one
+ } else if (index !== serviceAddress.length - 1) { // else if question mark not at end of string
+ index = serviceAddress.search(/&$/);
+ if (index < 0) {
+ serviceAddress = serviceAddress + "&"; // add a parameter separator
+ }
+ }
+
+ return serviceAddress;
+};
+
+export default WmsUrlBuilder;
diff --git a/web/test/WebWorldWind/src/util/libtess.js b/web/test/WebWorldWind/src/util/libtess.js
new file mode 100644
index 00000000..787faed1
--- /dev/null
+++ b/web/test/WebWorldWind/src/util/libtess.js
@@ -0,0 +1,5272 @@
+/*
+ * @license
+ * Copyright 2000, Silicon Graphics, Inc. All Rights Reserved.
+ * Copyright 2014, Google Inc. All Rights Reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice including the dates of first publication and
+ * either this permission notice or a reference to http://oss.sgi.com/projects/FreeB/
+ * shall be included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * SILICON GRAPHICS, INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+ * IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * Original Code. The Original Code is: OpenGL Sample Implementation,
+ * Version 1.2.1, released January 26, 2000, developed by Silicon Graphics,
+ * Inc. The Original Code is Copyright (c) 1991-2000 Silicon Graphics, Inc.
+ * Copyright in any portions created by third parties is as indicated
+ * elsewhere herein. All Rights Reserved.
+ */
+/*
+ * @author ericv@cs.stanford.edu (Eric Veach)
+ * @author bckenny@google.com (Brendan Kenny)
+ */
+
+/*
+ * Base namespace.
+ * @const
+ */
+var libtess = {};
+
+/**
+ * Whether to run asserts and extra debug checks.
+ * @define {boolean}
+ */
+libtess.DEBUG = false;
+
+/**
+ * Checks if the condition evaluates to true if libtess.DEBUG is true.
+ * @param {*} condition The condition to check.
+ * @param {string=} opt_message Error message in case of failure.
+ * @throws {Error} Assertion failed, the condition evaluates to false.
+ */
+libtess.assert = function (condition, opt_message) {
+ if (libtess.DEBUG && !condition) {
+ throw new Error('Assertion failed' +
+ (opt_message ? ': ' + opt_message : ''));
+ }
+};
+
+/**
+ * The maximum vertex coordinate size, 1e150. Anything larger will trigger a
+ * GLU_TESS_COORD_TOO_LARGE error callback and the vertex will be clamped to
+ * this value for all tessellation calculations.
+ * @const {number}
+ */
+libtess.GLU_TESS_MAX_COORD = 1e150;
+// NOTE(bckenny): value from glu.pl generator
+
+/**
+ * Normally the polygon is projected to a plane perpendicular to one of the
+ * three coordinate axes before tessellating in 2d. This helps numerical
+ * accuracy by forgoing a transformation step by simply dropping one coordinate
+ * dimension.
+ *
+ * However, this can affect the placement of intersection points for non-axis-
+ * aligned polygons. Setting TRUE_PROJECT to true will instead project onto a
+ * plane actually perpendicular to the polygon's normal.
+ *
+ * NOTE(bckenny): I can find no instances on the internet in which this mode has
+ * been used, but it's difficult to search for. This was a compile-time setting
+ * in the original, so setting this as constant. If this is exposed in the
+ * public API, remove the ignore coverage directives on
+ * libtess.normal.projectPolygon and libtess.normal.normalize_.
+ * @const {boolean}
+ */
+libtess.TRUE_PROJECT = false;
+
+/**
+ * We cache vertex data for single-contour polygons so that we can try a
+ * quick-and-dirty decomposition first.
+ * @const {number}
+ */
+libtess.TESS_MAX_CACHE = 100;
+
+/**
+ * The default tolerance for merging features, 0, meaning vertices are only
+ * merged if they are exactly coincident
+ * If a higher tolerance is needed, significant rewriting will need to occur.
+ * See libtess.sweep.TOLERANCE_NONZERO_ as a starting place.
+ * @const {number}
+ */
+libtess.GLU_TESS_DEFAULT_TOLERANCE = 0;
+
+/**
+ * The input contours parition the plane into regions. A winding
+ * rule determines which of these regions are inside the polygon.
+ *
+ * For a single contour C, the winding number of a point x is simply
+ * the signed number of revolutions we make around x as we travel
+ * once around C (where CCW is positive). When there are several
+ * contours, the individual winding numbers are summed. This
+ * procedure associates a signed integer value with each point x in
+ * the plane. Note that the winding number is the same for all
+ * points in a single region.
+ *
+ * The winding rule classifies a region as "inside" if its winding
+ * number belongs to the chosen category (odd, nonzero, positive,
+ * negative, or absolute value of at least two). The current GLU
+ * tesselator implements the "odd" rule. The "nonzero" rule is another
+ * common way to define the interior. The other three rules are
+ * useful for polygon CSG operations.
+ * @enum {number}
+ */
+libtess.windingRule = {
+ // NOTE(bckenny): values from enumglu.spec
+ GLU_TESS_WINDING_ODD: 100130,
+ GLU_TESS_WINDING_NONZERO: 100131,
+ GLU_TESS_WINDING_POSITIVE: 100132,
+ GLU_TESS_WINDING_NEGATIVE: 100133,
+ GLU_TESS_WINDING_ABS_GEQ_TWO: 100134
+};
+
+/**
+ * The type of primitive return from a "begin" callback. GL_LINE_LOOP is only
+ * returned when GLU_TESS_BOUNDARY_ONLY is true. GL_TRIANGLE_STRIP and
+ * GL_TRIANGLE_FAN are no longer returned since 1.1.0 (see release notes).
+ * @enum {number}
+ */
+libtess.primitiveType = {
+ GL_LINE_LOOP: 2,
+ GL_TRIANGLES: 4,
+ GL_TRIANGLE_STRIP: 5,
+ GL_TRIANGLE_FAN: 6
+};
+
+/**
+ * The types of errors provided in the error callback.
+ * @enum {number}
+ */
+libtess.errorType = {
+ // TODO(bckenny) doc types
+ // NOTE(bckenny): values from enumglu.spec
+ GLU_TESS_MISSING_BEGIN_POLYGON: 100151,
+ GLU_TESS_MISSING_END_POLYGON: 100153,
+ GLU_TESS_MISSING_BEGIN_CONTOUR: 100152,
+ GLU_TESS_MISSING_END_CONTOUR: 100154,
+ GLU_TESS_COORD_TOO_LARGE: 100155,
+ GLU_TESS_NEED_COMBINE_CALLBACK: 100156
+};
+
+/**
+ * Enum values necessary for providing settings and callbacks. See the readme
+ * for details.
+ * @enum {number}
+ */
+libtess.gluEnum = {
+ // TODO(bckenny): rename so not always typing libtess.gluEnum.*?
+
+ // NOTE(bckenny): values from enumglu.spec
+ GLU_TESS_BEGIN: 100100,
+ GLU_TESS_VERTEX: 100101,
+ GLU_TESS_END: 100102,
+ GLU_TESS_ERROR: 100103,
+ GLU_TESS_EDGE_FLAG: 100104,
+ GLU_TESS_COMBINE: 100105,
+ GLU_TESS_BEGIN_DATA: 100106,
+ GLU_TESS_VERTEX_DATA: 100107,
+ GLU_TESS_END_DATA: 100108,
+ GLU_TESS_ERROR_DATA: 100109,
+ GLU_TESS_EDGE_FLAG_DATA: 100110,
+ GLU_TESS_COMBINE_DATA: 100111,
+
+ GLU_TESS_MESH: 100112, // NOTE(bckenny): from tess.c
+ GLU_TESS_TOLERANCE: 100142,
+ GLU_TESS_WINDING_RULE: 100140,
+ GLU_TESS_BOUNDARY_ONLY: 100141,
+
+ // TODO(bckenny): move this to libtess.errorType?
+ GLU_INVALID_ENUM: 100900,
+ GLU_INVALID_VALUE: 100901
+};
+
+/** @typedef {number} */
+libtess.PQHandle;
+
+// TODO(bckenny): better typing on key?
+/** @typedef {Object} */
+libtess.PQKey;
+
+
+/* global libtess */
+
+/** @const */
+libtess.geom = {};
+
+/**
+ * Returns whether vertex u and vertex v are equal.
+ * @param {libtess.GluVertex} u
+ * @param {libtess.GluVertex} v
+ * @return {boolean}
+ */
+libtess.geom.vertEq = function (u, v) {
+ return u.s === v.s && u.t === v.t;
+};
+
+/**
+ * Returns whether vertex u is lexicographically less than or equal to vertex v.
+ * @param {libtess.GluVertex} u
+ * @param {libtess.GluVertex} v
+ * @return {boolean}
+ */
+libtess.geom.vertLeq = function (u, v) {
+ return u.s < v.s || u.s === v.s && u.t <= v.t;
+};
+
+/**
+ * Given three vertices u,v,w such that geom.vertLeq(u,v) && geom.vertLeq(v,w),
+ * evaluates the t-coord of the edge uw at the s-coord of the vertex v.
+ * Returns v.t - (uw)(v.s), ie. the signed distance from uw to v.
+ * If uw is vertical (and thus passes thru v), the result is zero.
+ *
+ * The calculation is extremely accurate and stable, even when v
+ * is very close to u or w. In particular if we set v.t = 0 and
+ * let r be the negated result (this evaluates (uw)(v.s)), then
+ * r is guaranteed to satisfy MIN(u.t,w.t) <= r <= MAX(u.t,w.t).
+ * @param {libtess.GluVertex} u
+ * @param {libtess.GluVertex} v
+ * @param {libtess.GluVertex} w
+ * @return {number}
+ */
+libtess.geom.edgeEval = function (u, v, w) {
+
+ var gapL = v.s - u.s;
+ var gapR = w.s - v.s;
+
+ if (gapL + gapR > 0) {
+ if (gapL < gapR) {
+ return v.t - u.t + (u.t - w.t) * (gapL / (gapL + gapR));
+ } else {
+ return v.t - w.t + (w.t - u.t) * (gapR / (gapL + gapR));
+ }
+ }
+
+ // vertical line
+ return 0;
+};
+
+/**
+ * Returns a number whose sign matches geom.edgeEval(u,v,w) but which
+ * is cheaper to evaluate. Returns > 0, == 0 , or < 0
+ * as v is above, on, or below the edge uw.
+ * @param {libtess.GluVertex} u
+ * @param {libtess.GluVertex} v
+ * @param {libtess.GluVertex} w
+ * @return {number}
+ */
+libtess.geom.edgeSign = function (u, v, w) {
+
+ var gapL = v.s - u.s;
+ var gapR = w.s - v.s;
+
+ if (gapL + gapR > 0) {
+ return (v.t - w.t) * gapL + (v.t - u.t) * gapR;
+ }
+
+ // vertical line
+ return 0;
+};
+
+/**
+ * Version of VertLeq with s and t transposed.
+ * Returns whether vertex u is lexicographically less than or equal to vertex v.
+ * @param {libtess.GluVertex} u
+ * @param {libtess.GluVertex} v
+ * @return {boolean}
+ */
+libtess.geom.transLeq = function (u, v) {
+ return u.t < v.t || u.t === v.t && u.s <= v.s;
+};
+
+/**
+ * Version of geom.edgeEval with s and t transposed.
+ * Given three vertices u,v,w such that geom.transLeq(u,v) &&
+ * geom.transLeq(v,w), evaluates the t-coord of the edge uw at the s-coord of
+ * the vertex v. Returns v.s - (uw)(v.t), ie. the signed distance from uw to v.
+ * If uw is vertical (and thus passes thru v), the result is zero.
+ *
+ * The calculation is extremely accurate and stable, even when v
+ * is very close to u or w. In particular if we set v.s = 0 and
+ * let r be the negated result (this evaluates (uw)(v.t)), then
+ * r is guaranteed to satisfy MIN(u.s,w.s) <= r <= MAX(u.s,w.s).
+ * @param {libtess.GluVertex} u
+ * @param {libtess.GluVertex} v
+ * @param {libtess.GluVertex} w
+ * @return {number}
+ */
+libtess.geom.transEval = function (u, v, w) {
+
+ var gapL = v.t - u.t;
+ var gapR = w.t - v.t;
+
+ if (gapL + gapR > 0) {
+ if (gapL < gapR) {
+ return v.s - u.s + (u.s - w.s) * (gapL / (gapL + gapR));
+ } else {
+ return v.s - w.s + (w.s - u.s) * (gapR / (gapL + gapR));
+ }
+ }
+
+ // vertical line
+ return 0;
+};
+
+/**
+ * Version of geom.edgeSign with s and t transposed.
+ * Returns a number whose sign matches geom.transEval(u,v,w) but which
+ * is cheaper to evaluate. Returns > 0, == 0 , or < 0
+ * as v is above, on, or below the edge uw.
+ * @param {libtess.GluVertex} u
+ * @param {libtess.GluVertex} v
+ * @param {libtess.GluVertex} w
+ * @return {number}
+ */
+libtess.geom.transSign = function (u, v, w) {
+
+ var gapL = v.t - u.t;
+ var gapR = w.t - v.t;
+
+ if (gapL + gapR > 0) {
+ return (v.s - w.s) * gapL + (v.s - u.s) * gapR;
+ }
+
+ // vertical line
+ return 0;
+};
+
+/**
+ * Returns whether edge is directed from right to left.
+ * @param {libtess.GluHalfEdge} e
+ * @return {boolean}
+ */
+libtess.geom.edgeGoesLeft = function (e) {
+ return libtess.geom.vertLeq(e.dst(), e.org);
+};
+
+/**
+ * Returns whether edge is directed from left to right.
+ * @param {libtess.GluHalfEdge} e
+ * @return {boolean}
+ */
+libtess.geom.edgeGoesRight = function (e) {
+ return libtess.geom.vertLeq(e.org, e.dst());
+};
+
+/**
+ * Calculates the L1 distance between vertices u and v.
+ * @param {libtess.GluVertex} u
+ * @param {libtess.GluVertex} v
+ * @return {number}
+ */
+libtess.geom.vertL1dist = function (u, v) {
+ return Math.abs(u.s - v.s) + Math.abs(u.t - v.t);
+};
+
+// NOTE(bckenny): vertCCW is called nowhere in libtess and isn't part of the
+// public API.
+/* istanbul ignore next */
+/**
+ * For almost-degenerate situations, the results are not reliable.
+ * Unless the floating-point arithmetic can be performed without
+ * rounding errors, *any* implementation will give incorrect results
+ * on some degenerate inputs, so the client must have some way to
+ * handle this situation.
+ * @param {!libtess.GluVertex} u
+ * @param {!libtess.GluVertex} v
+ * @param {!libtess.GluVertex} w
+ * @return {boolean}
+ */
+libtess.geom.vertCCW = function (u, v, w) {
+ return u.s * (v.t - w.t) + v.s * (w.t - u.t) + w.s * (u.t - v.t) >= 0;
+};
+
+/**
+ * Given parameters a,x,b,y returns the value (b*x+a*y)/(a+b),
+ * or (x+y)/2 if a==b==0. It requires that a,b >= 0, and enforces
+ * this in the rare case that one argument is slightly negative.
+ * The implementation is extremely stable numerically.
+ * In particular it guarantees that the result r satisfies
+ * MIN(x,y) <= r <= MAX(x,y), and the results are very accurate
+ * even when a and b differ greatly in magnitude.
+ * @private
+ * @param {number} a
+ * @param {number} x
+ * @param {number} b
+ * @param {number} y
+ * @return {number}
+ */
+libtess.geom.interpolate_ = function (a, x, b, y) {
+ // from Macro RealInterpolate:
+ //(a = (a < 0) ? 0 : a, b = (b < 0) ? 0 : b, ((a <= b) ? ((b == 0) ? ((x+y) / 2) : (x + (y-x) * (a/(a+b)))) : (y + (x-y) * (b/(a+b)))))
+ a = a < 0 ? 0 : a;
+ b = b < 0 ? 0 : b;
+
+ if (a <= b) {
+ if (b === 0) {
+ return (x + y) / 2;
+ } else {
+ return x + (y - x) * (a / (a + b));
+ }
+ } else {
+ return y + (x - y) * (b / (a + b));
+ }
+};
+
+/**
+ * Given edges (o1,d1) and (o2,d2), compute their point of intersection.
+ * The computed point is guaranteed to lie in the intersection of the
+ * bounding rectangles defined by each edge.
+ * @param {!libtess.GluVertex} o1
+ * @param {!libtess.GluVertex} d1
+ * @param {!libtess.GluVertex} o2
+ * @param {!libtess.GluVertex} d2
+ * @param {!libtess.GluVertex} v
+ */
+libtess.geom.edgeIntersect = function (o1, d1, o2, d2, v) {
+ // This is certainly not the most efficient way to find the intersection
+ // of two line segments, but it is very numerically stable.
+
+ // Strategy: find the two middle vertices in the VertLeq ordering,
+ // and interpolate the intersection s-value from these. Then repeat
+ // using the TransLeq ordering to find the intersection t-value.
+ var z1;
+ var z2;
+ var tmp;
+ if (!libtess.geom.vertLeq(o1, d1)) {
+ // Swap(o1, d1);
+ tmp = o1;
+ o1 = d1;
+ d1 = tmp;
+ }
+ if (!libtess.geom.vertLeq(o2, d2)) {
+ // Swap(o2, d2);
+ tmp = o2;
+ o2 = d2;
+ d2 = tmp;
+ }
+ if (!libtess.geom.vertLeq(o1, o2)) {
+ // Swap(o1, o2);
+ tmp = o1;
+ o1 = o2;
+ o2 = tmp;
+ // Swap(d1, d2);
+ tmp = d1;
+ d1 = d2;
+ d2 = tmp;
+ }
+
+ if (!libtess.geom.vertLeq(o2, d1)) {
+ // Technically, no intersection -- do our best
+ v.s = (o2.s + d1.s) / 2;
+
+ } else if (libtess.geom.vertLeq(d1, d2)) {
+ // Interpolate between o2 and d1
+ z1 = libtess.geom.edgeEval(o1, o2, d1);
+ z2 = libtess.geom.edgeEval(o2, d1, d2);
+ if (z1 + z2 < 0) { z1 = -z1; z2 = -z2; }
+ v.s = libtess.geom.interpolate_(z1, o2.s, z2, d1.s);
+
+ } else {
+ // Interpolate between o2 and d2
+ z1 = libtess.geom.edgeSign(o1, o2, d1);
+ z2 = -libtess.geom.edgeSign(o1, d2, d1);
+ if (z1 + z2 < 0) { z1 = -z1; z2 = -z2; }
+ v.s = libtess.geom.interpolate_(z1, o2.s, z2, d2.s);
+ }
+
+ // Now repeat the process for t
+ if (!libtess.geom.transLeq(o1, d1)) {
+ // Swap(o1, d1);
+ tmp = o1;
+ o1 = d1;
+ d1 = tmp;
+ }
+ if (!libtess.geom.transLeq(o2, d2)) {
+ // Swap(o2, d2);
+ tmp = o2;
+ o2 = d2;
+ d2 = tmp;
+ }
+ if (!libtess.geom.transLeq(o1, o2)) {
+ // Swap(o1, o2);
+ tmp = o1;
+ o1 = o2;
+ o2 = tmp;
+ // Swap(d1, d2);
+ tmp = d1;
+ d1 = d2;
+ d2 = tmp;
+ }
+
+ if (!libtess.geom.transLeq(o2, d1)) {
+ // Technically, no intersection -- do our best
+ v.t = (o2.t + d1.t) / 2;
+
+ } else if (libtess.geom.transLeq(d1, d2)) {
+ // Interpolate between o2 and d1
+ z1 = libtess.geom.transEval(o1, o2, d1);
+ z2 = libtess.geom.transEval(o2, d1, d2);
+ if (z1 + z2 < 0) { z1 = -z1; z2 = -z2; }
+ v.t = libtess.geom.interpolate_(z1, o2.t, z2, d1.t);
+
+ } else {
+ // Interpolate between o2 and d2
+ z1 = libtess.geom.transSign(o1, o2, d1);
+ z2 = -libtess.geom.transSign(o1, d2, d1);
+ if (z1 + z2 < 0) { z1 = -z1; z2 = -z2; }
+ v.t = libtess.geom.interpolate_(z1, o2.t, z2, d2.t);
+ }
+};
+
+
+
+/* global libtess */
+
+// TODO(bckenny): could maybe merge GluMesh and mesh.js since these are
+// operations on the mesh
+
+/** @const */
+libtess.mesh = {};
+
+/****************** Basic Edge Operations **********************/
+
+
+/**
+ * makeEdge creates one edge, two vertices, and a loop (face).
+ * The loop consists of the two new half-edges.
+ *
+ * @param {libtess.GluMesh} mesh [description].
+ * @return {libtess.GluHalfEdge} [description].
+ */
+libtess.mesh.makeEdge = function (mesh) {
+ // TODO(bckenny): probably move to GluMesh, but needs Make* methods with it
+
+ var e = libtess.mesh.makeEdgePair_(mesh.eHead);
+
+ // complete edge with vertices and face (see mesh.makeEdgePair_)
+ libtess.mesh.makeVertex_(e, mesh.vHead);
+ libtess.mesh.makeVertex_(e.sym, mesh.vHead);
+ libtess.mesh.makeFace_(e, mesh.fHead);
+
+ return e;
+};
+
+
+/**
+ * meshSplice(eOrg, eDst) is the basic operation for changing the
+ * mesh connectivity and topology. It changes the mesh so that
+ * eOrg.oNext <- OLD( eDst.oNext )
+ * eDst.oNext <- OLD( eOrg.oNext )
+ * where OLD(...) means the value before the meshSplice operation.
+ *
+ * This can have two effects on the vertex structure:
+ * - if eOrg.org != eDst.org, the two vertices are merged together
+ * - if eOrg.org == eDst.org, the origin is split into two vertices
+ * In both cases, eDst.org is changed and eOrg.org is untouched.
+ *
+ * Similarly (and independently) for the face structure,
+ * - if eOrg.lFace == eDst.lFace, one loop is split into two
+ * - if eOrg.lFace != eDst.lFace, two distinct loops are joined into one
+ * In both cases, eDst.lFace is changed and eOrg.lFace is unaffected.
+ *
+ * Some special cases:
+ * If eDst == eOrg, the operation has no effect.
+ * If eDst == eOrg.lNext, the new face will have a single edge.
+ * If eDst == eOrg.lPrev(), the old face will have a single edge.
+ * If eDst == eOrg.oNext, the new vertex will have a single edge.
+ * If eDst == eOrg.oPrev(), the old vertex will have a single edge.
+ *
+ * @param {libtess.GluHalfEdge} eOrg [description].
+ * @param {libtess.GluHalfEdge} eDst [description].
+ */
+libtess.mesh.meshSplice = function (eOrg, eDst) {
+ // TODO: more descriptive name?
+
+ var joiningLoops = false;
+ var joiningVertices = false;
+
+ if (eOrg === eDst) {
+ return;
+ }
+
+ if (eDst.org !== eOrg.org) {
+ // We are merging two disjoint vertices -- destroy eDst.org
+ joiningVertices = true;
+ libtess.mesh.killVertex_(eDst.org, eOrg.org);
+ }
+
+ if (eDst.lFace !== eOrg.lFace) {
+ // We are connecting two disjoint loops -- destroy eDst.lFace
+ joiningLoops = true;
+ libtess.mesh.killFace_(eDst.lFace, eOrg.lFace);
+ }
+
+ // Change the edge structure
+ libtess.mesh.splice_(eDst, eOrg);
+
+ if (!joiningVertices) {
+ // We split one vertex into two -- the new vertex is eDst.org.
+ // Make sure the old vertex points to a valid half-edge.
+ libtess.mesh.makeVertex_(eDst, eOrg.org);
+ eOrg.org.anEdge = eOrg;
+ }
+
+ if (!joiningLoops) {
+ // We split one loop into two -- the new loop is eDst.lFace.
+ // Make sure the old face points to a valid half-edge.
+ libtess.mesh.makeFace_(eDst, eOrg.lFace);
+ eOrg.lFace.anEdge = eOrg;
+ }
+};
+
+
+/**
+ * deleteEdge(eDel) removes the edge eDel. There are several cases:
+ * if (eDel.lFace != eDel.rFace()), we join two loops into one; the loop
+ * eDel.lFace is deleted. Otherwise, we are splitting one loop into two;
+ * the newly created loop will contain eDel.dst(). If the deletion of eDel
+ * would create isolated vertices, those are deleted as well.
+ *
+ * This function could be implemented as two calls to __gl_meshSplice
+ * plus a few calls to memFree, but this would allocate and delete
+ * unnecessary vertices and faces.
+ *
+ * @param {libtess.GluHalfEdge} eDel [description].
+ */
+libtess.mesh.deleteEdge = function (eDel) {
+ var eDelSym = eDel.sym;
+ var joiningLoops = false;
+
+ // First step: disconnect the origin vertex eDel.org. We make all
+ // changes to get a consistent mesh in this "intermediate" state.
+ if (eDel.lFace !== eDel.rFace()) {
+ // We are joining two loops into one -- remove the left face
+ joiningLoops = true;
+ libtess.mesh.killFace_(eDel.lFace, eDel.rFace());
+ }
+
+ if (eDel.oNext === eDel) {
+ libtess.mesh.killVertex_(eDel.org, null);
+
+ } else {
+ // Make sure that eDel.org and eDel.rFace() point to valid half-edges
+ eDel.rFace().anEdge = eDel.oPrev();
+ eDel.org.anEdge = eDel.oNext;
+
+ libtess.mesh.splice_(eDel, eDel.oPrev());
+
+ if (!joiningLoops) {
+ // We are splitting one loop into two -- create a new loop for eDel.
+ libtess.mesh.makeFace_(eDel, eDel.lFace);
+ }
+ }
+
+ // Claim: the mesh is now in a consistent state, except that eDel.org
+ // may have been deleted. Now we disconnect eDel.dst().
+ if (eDelSym.oNext === eDelSym) {
+ libtess.mesh.killVertex_(eDelSym.org, null);
+ libtess.mesh.killFace_(eDelSym.lFace, null);
+
+ } else {
+ // Make sure that eDel.dst() and eDel.lFace point to valid half-edges
+ eDel.lFace.anEdge = eDelSym.oPrev();
+ eDelSym.org.anEdge = eDelSym.oNext;
+ libtess.mesh.splice_(eDelSym, eDelSym.oPrev());
+ }
+
+ // Any isolated vertices or faces have already been freed.
+ libtess.mesh.killEdge_(eDel);
+};
+
+/******************** Other Edge Operations **********************/
+
+/* All these routines can be implemented with the basic edge
+ * operations above. They are provided for convenience and efficiency.
+ */
+
+
+/**
+ * addEdgeVertex(eOrg) creates a new edge eNew such that
+ * eNew == eOrg.lNext, and eNew.dst() is a newly created vertex.
+ * eOrg and eNew will have the same left face.
+ *
+ * @param {libtess.GluHalfEdge} eOrg [description].
+ * @return {libtess.GluHalfEdge} [description].
+ */
+libtess.mesh.addEdgeVertex = function (eOrg) {
+ // TODO(bckenny): why is it named this?
+
+ var eNew = libtess.mesh.makeEdgePair_(eOrg);
+ var eNewSym = eNew.sym;
+
+ // Connect the new edge appropriately
+ libtess.mesh.splice_(eNew, eOrg.lNext);
+
+ // Set the vertex and face information
+ eNew.org = eOrg.dst();
+
+ libtess.mesh.makeVertex_(eNewSym, eNew.org);
+
+ eNew.lFace = eNewSym.lFace = eOrg.lFace;
+
+ return eNew;
+};
+
+
+/**
+ * splitEdge(eOrg) splits eOrg into two edges eOrg and eNew,
+ * such that eNew == eOrg.lNext. The new vertex is eOrg.dst() == eNew.org.
+ * eOrg and eNew will have the same left face.
+ *
+ * @param {libtess.GluHalfEdge} eOrg [description].
+ * @return {!libtess.GluHalfEdge} [description].
+ */
+libtess.mesh.splitEdge = function (eOrg) {
+ var tempHalfEdge = libtess.mesh.addEdgeVertex(eOrg);
+ var eNew = tempHalfEdge.sym;
+
+ // Disconnect eOrg from eOrg.dst() and connect it to eNew.org
+ libtess.mesh.splice_(eOrg.sym, eOrg.sym.oPrev());
+ libtess.mesh.splice_(eOrg.sym, eNew);
+
+ // Set the vertex and face information
+ eOrg.sym.org = eNew.org; // NOTE(bckenny): assignment to dst
+ eNew.dst().anEdge = eNew.sym; // may have pointed to eOrg.sym
+ eNew.sym.lFace = eOrg.rFace(); // NOTE(bckenny): assignment to rFace
+ eNew.winding = eOrg.winding; // copy old winding information
+ eNew.sym.winding = eOrg.sym.winding;
+
+ return eNew;
+};
+
+
+/**
+ * connect(eOrg, eDst) creates a new edge from eOrg.dst()
+ * to eDst.org, and returns the corresponding half-edge eNew.
+ * If eOrg.lFace == eDst.lFace, this splits one loop into two,
+ * and the newly created loop is eNew.lFace. Otherwise, two disjoint
+ * loops are merged into one, and the loop eDst.lFace is destroyed.
+ *
+ * If (eOrg == eDst), the new face will have only two edges.
+ * If (eOrg.lNext == eDst), the old face is reduced to a single edge.
+ * If (eOrg.lNext.lNext == eDst), the old face is reduced to two edges.
+ *
+ * @param {libtess.GluHalfEdge} eOrg [description].
+ * @param {libtess.GluHalfEdge} eDst [description].
+ * @return {!libtess.GluHalfEdge} [description].
+ */
+libtess.mesh.connect = function (eOrg, eDst) {
+ var joiningLoops = false;
+ var eNew = libtess.mesh.makeEdgePair_(eOrg);
+ var eNewSym = eNew.sym;
+
+ if (eDst.lFace !== eOrg.lFace) {
+ // We are connecting two disjoint loops -- destroy eDst.lFace
+ joiningLoops = true;
+ libtess.mesh.killFace_(eDst.lFace, eOrg.lFace);
+ }
+
+ // Connect the new edge appropriately
+ libtess.mesh.splice_(eNew, eOrg.lNext);
+ libtess.mesh.splice_(eNewSym, eDst);
+
+ // Set the vertex and face information
+ eNew.org = eOrg.dst();
+ eNewSym.org = eDst.org;
+ eNew.lFace = eNewSym.lFace = eOrg.lFace;
+
+ // Make sure the old face points to a valid half-edge
+ eOrg.lFace.anEdge = eNewSym;
+
+ if (!joiningLoops) {
+ // We split one loop into two -- the new loop is eNew.lFace
+ libtess.mesh.makeFace_(eNew, eOrg.lFace);
+ }
+ return eNew;
+};
+
+/******************** Other Operations **********************/
+
+
+/**
+ * zapFace(fZap) destroys a face and removes it from the
+ * global face list. All edges of fZap will have a null pointer as their
+ * left face. Any edges which also have a null pointer as their right face
+ * are deleted entirely (along with any isolated vertices this produces).
+ * An entire mesh can be deleted by zapping its faces, one at a time,
+ * in any order. Zapped faces cannot be used in further mesh operations!
+ *
+ * @param {libtess.GluFace} fZap [description].
+ */
+libtess.mesh.zapFace = function (fZap) {
+ var eStart = fZap.anEdge;
+
+ // walk around face, deleting edges whose right face is also NULL
+ var eNext = eStart.lNext;
+ var e;
+ do {
+ e = eNext;
+ eNext = e.lNext;
+
+ e.lFace = null;
+ if (e.rFace() === null) {
+ // delete the edge -- see mesh.deleteEdge above
+ if (e.oNext === e) {
+ libtess.mesh.killVertex_(e.org, null);
+
+ } else {
+ // Make sure that e.org points to a valid half-edge
+ e.org.anEdge = e.oNext;
+ libtess.mesh.splice_(e, e.oPrev());
+ }
+
+ var eSym = e.sym;
+
+ if (eSym.oNext === eSym) {
+ libtess.mesh.killVertex_(eSym.org, null);
+
+ } else {
+ // Make sure that eSym.org points to a valid half-edge
+ eSym.org.anEdge = eSym.oNext;
+ libtess.mesh.splice_(eSym, eSym.oPrev());
+ }
+ libtess.mesh.killEdge_(e);
+ }
+ } while (e !== eStart);
+
+ // delete from circular doubly-linked list
+ var fPrev = fZap.prev;
+ var fNext = fZap.next;
+ fNext.prev = fPrev;
+ fPrev.next = fNext;
+
+ // TODO(bckenny): memFree( fZap );
+ // TODO(bckenny): probably null at callsite
+};
+
+// TODO(bckenny): meshUnion isn't called within libtess and isn't part of the
+// public API. Could be useful if more mesh manipulation functions are exposed.
+/* istanbul ignore next */
+/**
+ * meshUnion() forms the union of all structures in
+ * both meshes, and returns the new mesh (the old meshes are destroyed).
+ *
+ * @param {!libtess.GluMesh} mesh1
+ * @param {!libtess.GluMesh} mesh2
+ * @return {!libtess.GluMesh}
+ */
+libtess.mesh.meshUnion = function (mesh1, mesh2) {
+ // TODO(bceknny): probably move to GluMesh method
+ var f1 = mesh1.fHead;
+ var v1 = mesh1.vHead;
+ var e1 = mesh1.eHead;
+
+ var f2 = mesh2.fHead;
+ var v2 = mesh2.vHead;
+ var e2 = mesh2.eHead;
+
+ // Add the faces, vertices, and edges of mesh2 to those of mesh1
+ if (f2.next !== f2) {
+ f1.prev.next = f2.next;
+ f2.next.prev = f1.prev;
+ f2.prev.next = f1;
+ f1.prev = f2.prev;
+ }
+
+ if (v2.next !== v2) {
+ v1.prev.next = v2.next;
+ v2.next.prev = v1.prev;
+ v2.prev.next = v1;
+ v1.prev = v2.prev;
+ }
+
+ if (e2.next !== e2) {
+ e1.sym.next.sym.next = e2.next;
+ e2.next.sym.next = e1.sym.next;
+ e2.sym.next.sym.next = e1;
+ e1.sym.next = e2.sym.next;
+ }
+
+ // TODO(bckenny): memFree(mesh2);
+ // TODO(bckenny): If function is kept, remove mesh2's data to enforce.
+ return mesh1;
+};
+
+
+/**
+ * deleteMesh(mesh) will free all storage for any valid mesh.
+ * @param {libtess.GluMesh} mesh [description].
+ */
+libtess.mesh.deleteMesh = function (mesh) {
+ // TODO(bckenny): unnecessary, I think.
+ // TODO(bckenny): might want to explicitly null at callsite
+ // lots of memFrees. see also DELETE_BY_ZAPPING
+};
+
+/************************ Utility Routines ************************/
+
+
+/**
+ * Creates a new pair of half-edges which form their own loop.
+ * No vertex or face structures are allocated, but these must be assigned
+ * before the current edge operation is completed.
+ *
+ * TODO(bckenny): warning about eNext strictly being first of pair? (see code)
+ *
+ * @private
+ * @param {libtess.GluHalfEdge} eNext [description].
+ * @return {libtess.GluHalfEdge} [description].
+ */
+libtess.mesh.makeEdgePair_ = function (eNext) {
+ var e = new libtess.GluHalfEdge();
+ var eSym = new libtess.GluHalfEdge();
+
+ // TODO(bckenny): how do we ensure this? see above comment in jsdoc
+ // Make sure eNext points to the first edge of the edge pair
+ // if (eNext->Sym < eNext ) { eNext = eNext->Sym; }
+
+ // NOTE(bckenny): check this for bugs in current implementation!
+
+ // Insert in circular doubly-linked list before eNext.
+ // Note that the prev pointer is stored in sym.next.
+ var ePrev = eNext.sym.next;
+ eSym.next = ePrev;
+ ePrev.sym.next = e;
+ e.next = eNext;
+ eNext.sym.next = eSym;
+
+ e.sym = eSym;
+ e.oNext = e;
+ e.lNext = eSym;
+
+ eSym.sym = e;
+ eSym.oNext = eSym;
+ eSym.lNext = e;
+
+ return e;
+};
+
+
+/**
+ * splice_ is best described by the Guibas/Stolfi paper or the
+ * CS348a notes. Basically, it modifies the mesh so that
+ * a.oNext and b.oNext are exchanged. This can have various effects
+ * depending on whether a and b belong to different face or vertex rings.
+ * For more explanation see mesh.meshSplice below.
+ *
+ * @private
+ * @param {libtess.GluHalfEdge} a [description].
+ * @param {libtess.GluHalfEdge} b [description].
+ */
+libtess.mesh.splice_ = function (a, b) {
+ var aONext = a.oNext;
+ var bONext = b.oNext;
+
+ aONext.sym.lNext = b;
+ bONext.sym.lNext = a;
+ a.oNext = bONext;
+ b.oNext = aONext;
+};
+
+
+/**
+ * makeVertex_(eOrig, vNext) attaches a new vertex and makes it the
+ * origin of all edges in the vertex loop to which eOrig belongs. "vNext" gives
+ * a place to insert the new vertex in the global vertex list. We insert
+ * the new vertex *before* vNext so that algorithms which walk the vertex
+ * list will not see the newly created vertices.
+ *
+ * NOTE: unlike original, acutally allocates new vertex.
+ *
+ * @private
+ * @param {libtess.GluHalfEdge} eOrig [description].
+ * @param {libtess.GluVertex} vNext [description].
+ */
+libtess.mesh.makeVertex_ = function (eOrig, vNext) {
+ // insert in circular doubly-linked list before vNext
+ var vPrev = vNext.prev;
+ var vNew = new libtess.GluVertex(vNext, vPrev);
+ vPrev.next = vNew;
+ vNext.prev = vNew;
+
+ vNew.anEdge = eOrig;
+ // leave coords, s, t undefined
+ // TODO(bckenny): does above line mean 0 specifically, or does it matter?
+
+ // fix other edges on this vertex loop
+ var e = eOrig;
+ do {
+ e.org = vNew;
+ e = e.oNext;
+ } while (e !== eOrig);
+};
+
+
+/**
+ * makeFace_(eOrig, fNext) attaches a new face and makes it the left
+ * face of all edges in the face loop to which eOrig belongs. "fNext" gives
+ * a place to insert the new face in the global face list. We insert
+ * the new face *before* fNext so that algorithms which walk the face
+ * list will not see the newly created faces.
+ *
+ * NOTE: unlike original, acutally allocates new face.
+ *
+ * @private
+ * @param {libtess.GluHalfEdge} eOrig [description].
+ * @param {libtess.GluFace} fNext [description].
+ */
+libtess.mesh.makeFace_ = function (eOrig, fNext) {
+ // insert in circular doubly-linked list before fNext
+ var fPrev = fNext.prev;
+ var fNew = new libtess.GluFace(fNext, fPrev);
+ fPrev.next = fNew;
+ fNext.prev = fNew;
+
+ fNew.anEdge = eOrig;
+
+ // The new face is marked "inside" if the old one was. This is a
+ // convenience for the common case where a face has been split in two.
+ fNew.inside = fNext.inside;
+
+ // fix other edges on this face loop
+ var e = eOrig;
+ do {
+ e.lFace = fNew;
+ e = e.lNext;
+ } while (e !== eOrig);
+};
+
+
+/**
+ * killEdge_ destroys an edge (the half-edges eDel and eDel.sym),
+ * and removes from the global edge list.
+ *
+ * @private
+ * @param {libtess.GluHalfEdge} eDel [description].
+ */
+libtess.mesh.killEdge_ = function (eDel) {
+ // TODO(bckenny): in this case, no need to worry(?), but check when checking mesh.makeEdgePair_
+ // Half-edges are allocated in pairs, see EdgePair above
+ // if (eDel->Sym < eDel ) { eDel = eDel->Sym; }
+
+ // delete from circular doubly-linked list
+ var eNext = eDel.next;
+ var ePrev = eDel.sym.next;
+ eNext.sym.next = ePrev;
+ ePrev.sym.next = eNext;
+
+ // TODO(bckenny): memFree( eDel ); (which also frees eDel.sym)
+ // TODO(bckenny): need to null at callsites?
+};
+
+
+/**
+ * killVertex_ destroys a vertex and removes it from the global
+ * vertex list. It updates the vertex loop to point to a given new vertex.
+ *
+ * @private
+ * @param {libtess.GluVertex} vDel [description].
+ * @param {libtess.GluVertex} newOrg [description].
+ */
+libtess.mesh.killVertex_ = function (vDel, newOrg) {
+ var eStart = vDel.anEdge;
+
+ // change the origin of all affected edges
+ var e = eStart;
+ do {
+ e.org = newOrg;
+ e = e.oNext;
+ } while (e !== eStart);
+
+ // delete from circular doubly-linked list
+ var vPrev = vDel.prev;
+ var vNext = vDel.next;
+ vNext.prev = vPrev;
+ vPrev.next = vNext;
+
+ // TODO(bckenny): memFree( vDel );
+ // TODO(bckenny): need to null at callsites?
+};
+
+
+/**
+ * killFace_ destroys a face and removes it from the global face
+ * list. It updates the face loop to point to a given new face.
+ *
+ * @private
+ * @param {libtess.GluFace} fDel [description].
+ * @param {libtess.GluFace} newLFace [description].
+ */
+libtess.mesh.killFace_ = function (fDel, newLFace) {
+ var eStart = fDel.anEdge;
+
+ // change the left face of all affected edges
+ var e = eStart;
+ do {
+ e.lFace = newLFace;
+ e = e.lNext;
+ } while (e !== eStart);
+
+ // delete from circular doubly-linked list
+ var fPrev = fDel.prev;
+ var fNext = fDel.next;
+ fNext.prev = fPrev;
+ fPrev.next = fNext;
+
+ // TODO(bckenny): memFree( fDel );
+ // TODO(bckenny): need to null at callsites?
+};
+
+
+/* global libtess */
+
+/** @const */
+libtess.normal = {};
+
+// TODO(bckenny): Integrate SLANTED_SWEEP somehow?
+/* The "feature merging" is not intended to be complete. There are
+ * special cases where edges are nearly parallel to the sweep line
+ * which are not implemented. The algorithm should still behave
+ * robustly (ie. produce a reasonable tesselation) in the presence
+ * of such edges, however it may miss features which could have been
+ * merged. We could minimize this effect by choosing the sweep line
+ * direction to be something unusual (ie. not parallel to one of the
+ * coordinate axes).
+ * #if defined(SLANTED_SWEEP)
+ * #define S_UNIT_X 0.50941539564955385 // Pre-normalized
+ * #define S_UNIT_Y 0.86052074622010633
+ * #endif
+ */
+
+/**
+ * X coordinate of local basis for polygon projection.
+ * @private
+ * @const
+ */
+libtess.normal.S_UNIT_X_ = 1.0;
+
+/**
+ * Y coordinate of local basis for polygon projection.
+ * @private
+ * @const
+ */
+libtess.normal.S_UNIT_Y_ = 0.0;
+
+/**
+ * Determines a polygon normal and projects vertices onto the plane of the
+ * polygon.
+ * @param {!libtess.GluTesselator} tess
+ */
+libtess.normal.projectPolygon = function (tess) {
+ var computedNormal = false;
+
+ var norm = [
+ tess.normal[0],
+ tess.normal[1],
+ tess.normal[2]
+ ];
+ if (norm[0] === 0 && norm[1] === 0 && norm[2] === 0) {
+ libtess.normal.computeNormal_(tess, norm);
+ computedNormal = true;
+ }
+
+ var sUnit = tess.sUnit;
+ var tUnit = tess.tUnit;
+ var i = libtess.normal.longAxis_(norm);
+
+ // NOTE(bckenny): This branch is never taken. See comment on
+ // libtess.TRUE_PROJECT.
+ /* istanbul ignore if */
+ if (libtess.TRUE_PROJECT) {
+ // Choose the initial sUnit vector to be approximately perpendicular
+ // to the normal.
+ libtess.normal.normalize_(norm);
+
+ sUnit[i] = 0;
+ sUnit[(i + 1) % 3] = libtess.normal.S_UNIT_X_;
+ sUnit[(i + 2) % 3] = libtess.normal.S_UNIT_Y_;
+
+ // Now make it exactly perpendicular
+ var w = libtess.normal.dot_(sUnit, norm);
+ sUnit[0] -= w * norm[0];
+ sUnit[1] -= w * norm[1];
+ sUnit[2] -= w * norm[2];
+ libtess.normal.normalize_(sUnit);
+
+ // Choose tUnit so that (sUnit,tUnit,norm) form a right-handed frame
+ tUnit[0] = norm[1] * sUnit[2] - norm[2] * sUnit[1];
+ tUnit[1] = norm[2] * sUnit[0] - norm[0] * sUnit[2];
+ tUnit[2] = norm[0] * sUnit[1] - norm[1] * sUnit[0];
+ libtess.normal.normalize_(tUnit);
+
+ } else {
+ // Project perpendicular to a coordinate axis -- better numerically
+ sUnit[i] = 0;
+ sUnit[(i + 1) % 3] = libtess.normal.S_UNIT_X_;
+ sUnit[(i + 2) % 3] = libtess.normal.S_UNIT_Y_;
+
+ tUnit[i] = 0;
+ tUnit[(i + 1) % 3] = norm[i] > 0 ?
+ -libtess.normal.S_UNIT_Y_ : libtess.normal.S_UNIT_Y_;
+ tUnit[(i + 2) % 3] = norm[i] > 0 ?
+ libtess.normal.S_UNIT_X_ : -libtess.normal.S_UNIT_X_;
+ }
+
+ // Project the vertices onto the sweep plane
+ var vHead = tess.mesh.vHead;
+ for (var v = vHead.next; v !== vHead; v = v.next) {
+ v.s = libtess.normal.dot_(v.coords, sUnit);
+ v.t = libtess.normal.dot_(v.coords, tUnit);
+ }
+
+ if (computedNormal) {
+ libtess.normal.checkOrientation_(tess);
+ }
+};
+
+/**
+ * Computes the dot product of vectors u and v.
+ * @private
+ * @param {!Array.