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 @@ + + + + + + + World Wind + + + + + + + + + + \ No newline at end of file diff --git a/web/test/WebWorldWind/rollup.config.js b/web/test/WebWorldWind/rollup.config.js new file mode 100644 index 00000000..9b122b81 --- /dev/null +++ b/web/test/WebWorldWind/rollup.config.js @@ -0,0 +1,49 @@ +import commonjs from 'rollup-plugin-commonjs'; +import resolve from 'rollup-plugin-node-resolve'; +import strip from 'rollup-plugin-strip-banner'; +import bundleWorker from 'rollup-plugin-bundle-worker'; + +function glsl() { + return { + transform(code, id) { + if (/\.glsl$/.test(id) === false) return; + + var transformedCode = 'export default ' + JSON.stringify( + code + .replace(/[ \t]*\/\/.*\n/g, '') // remove // + .replace(/[ \t]*\/\*[\s\S]*?\*\//g, '') // remove /* */ + .replace(/\n{2,}/g, '\n') // # \n+ to \n + ) + ';'; + return { + code: transformedCode, + map: { + mappings: '' + } + }; + } + }; +} + +export default { + input: 'src/WorldWind.js', + output: { + indent: '\t', + format: 'umd', + name: 'WorldWind', + file: './build/WorldWind.js' + }, + treeshake: true, + external: [], + plugins: [ + bundleWorker(), + glsl(), + resolve(), + commonjs(), + strip() + ], + onwarn(warning, rollupWarn) { + if (warning.code !== 'CIRCULAR_DEPENDENCY') { + rollupWarn(warning); + } + } +}; diff --git a/web/test/WebWorldWind/src/BasicWorldWindowController.js b/web/test/WebWorldWind/src/BasicWorldWindowController.js new file mode 100644 index 00000000..660ea1dc --- /dev/null +++ b/web/test/WebWorldWind/src/BasicWorldWindowController.js @@ -0,0 +1,408 @@ +/* + * 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 BasicWorldWindowController + */ +import Angle from './geom/Angle'; +import DragRecognizer from './gesture/DragRecognizer'; +import GestureRecognizer from './gesture/GestureRecognizer'; +import Matrix from './geom/Matrix'; +import PanRecognizer from './gesture/PanRecognizer'; +import PinchRecognizer from './gesture/PinchRecognizer'; +import RotationRecognizer from './gesture/RotationRecognizer'; +import TiltRecognizer from './gesture/TiltRecognizer'; +import Vec2 from './geom/Vec2'; +import Vec3 from './geom/Vec3'; +import WorldWindowController from './WorldWindowController'; +import WWMath from './util/WWMath'; + + +/** + * Constructs a window controller with basic capabilities. + * @alias BasicWorldWindowController + * @constructor + * @augments WorldWindowController + * @classDesc This class provides the default window controller for WorldWind for controlling the globe via user interaction. + * @param {WorldWindow} worldWindow The WorldWindow associated with this layer. + */ +function BasicWorldWindowController(worldWindow) { + WorldWindowController.call(this, worldWindow); // base class checks for a valid worldWindow + + // Intentionally not documented. + this.primaryDragRecognizer = new DragRecognizer(this.wwd, null); + this.primaryDragRecognizer.addListener(this); + + // Intentionally not documented. + this.secondaryDragRecognizer = new DragRecognizer(this.wwd, null); + this.secondaryDragRecognizer.addListener(this); + this.secondaryDragRecognizer.button = 2; // secondary mouse button + + // Intentionally not documented. + this.panRecognizer = new PanRecognizer(this.wwd, null); + this.panRecognizer.addListener(this); + + // Intentionally not documented. + this.pinchRecognizer = new PinchRecognizer(this.wwd, null); + this.pinchRecognizer.addListener(this); + + // Intentionally not documented. + this.rotationRecognizer = new RotationRecognizer(this.wwd, null); + this.rotationRecognizer.addListener(this); + + // Intentionally not documented. + this.tiltRecognizer = new TiltRecognizer(this.wwd, null); + this.tiltRecognizer.addListener(this); + + // Establish the dependencies between gesture recognizers. The pan, pinch and rotate gesture may recognize + // simultaneously with each other. + this.panRecognizer.recognizeSimultaneouslyWith(this.pinchRecognizer); + this.panRecognizer.recognizeSimultaneouslyWith(this.rotationRecognizer); + this.pinchRecognizer.recognizeSimultaneouslyWith(this.rotationRecognizer); + + // Since the tilt gesture is a subset of the pan gesture, pan will typically recognize before tilt, + // effectively suppressing tilt. Establish a dependency between the other touch gestures and tilt to provide + // tilt an opportunity to recognize. + this.panRecognizer.requireRecognizerToFail(this.tiltRecognizer); + this.pinchRecognizer.requireRecognizerToFail(this.tiltRecognizer); + this.rotationRecognizer.requireRecognizerToFail(this.tiltRecognizer); + + // Intentionally not documented. + // this.tapRecognizer = new TapRecognizer(this.wwd, null); + // this.tapRecognizer.addListener(this); + + // Intentionally not documented. + // this.clickRecognizer = new ClickRecognizer(this.wwd, null); + // this.clickRecognizer.addListener(this); + + // Intentionally not documented. + this.beginPoint = new Vec2(0, 0); + this.lastPoint = new Vec2(0, 0); + this.beginHeading = 0; + this.beginTilt = 0; + this.beginRange = 0; + this.lastRotation = 0; +} + +BasicWorldWindowController.prototype = Object.create(WorldWindowController.prototype); + +// Intentionally not documented. +BasicWorldWindowController.prototype.onGestureEvent = function (e) { + var handled = WorldWindowController.prototype.onGestureEvent.call(this, e); + + if (!handled) { + if (e.type === "wheel") { + handled = true; + this.handleWheelEvent(e); + } + else { + for (var i = 0, len = GestureRecognizer.allRecognizers.length; i < len; i++) { + var recognizer = GestureRecognizer.allRecognizers[i]; + if (recognizer.target === this.wwd) { + handled |= recognizer.onGestureEvent(e); // use or-assignment to indicate if any recognizer handled the event + } + } + } + } + + return handled; +}; + +// Intentionally not documented. +BasicWorldWindowController.prototype.gestureStateChanged = function (recognizer) { + if (recognizer === this.primaryDragRecognizer || recognizer === this.panRecognizer) { + this.handlePanOrDrag(recognizer); + } + else if (recognizer === this.secondaryDragRecognizer) { + this.handleSecondaryDrag(recognizer); + } + else if (recognizer === this.pinchRecognizer) { + this.handlePinch(recognizer); + } + else if (recognizer === this.rotationRecognizer) { + this.handleRotation(recognizer); + } + else if (recognizer === this.tiltRecognizer) { + this.handleTilt(recognizer); + } + // else if (recognizer === this.clickRecognizer || recognizer === this.tapRecognizer) { + // this.handleClickOrTap(recognizer); + // } +}; + +// Intentionally not documented. +// BasicWorldWindowController.prototype.handleClickOrTap = function (recognizer) { +// if (recognizer.state === WorldWind.RECOGNIZED) { +// var pickPoint = this.wwd.canvasCoordinates(recognizer.clientX, recognizer.clientY); +// +// // Identify if the top picked object contains a URL for hyperlinking +// var pickList = this.wwd.pick(pickPoint); +// var topObject = pickList.topPickedObject(); +// // If the url object was appended, open the hyperlink +// if (topObject && +// topObject.userObject && +// topObject.userObject.userProperties && +// topObject.userObject.userProperties.url) { +// window.open(topObject.userObject.userProperties.url, "_blank"); +// } +// } +// }; + +// Intentionally not documented. +BasicWorldWindowController.prototype.handlePanOrDrag = function (recognizer) { + if (this.wwd.globe.is2D()) { + this.handlePanOrDrag2D(recognizer); + } else { + this.handlePanOrDrag3D(recognizer); + } +}; + +// Intentionally not documented. +BasicWorldWindowController.prototype.handlePanOrDrag3D = function (recognizer) { + var state = recognizer.state, + tx = recognizer.translationX, + ty = recognizer.translationY; + + var navigator = this.wwd.navigator; + if (state === WorldWind.BEGAN) { + this.lastPoint.set(0, 0); + } else if (state === WorldWind.CHANGED) { + // Convert the translation from screen coordinates to arc degrees. Use this navigator's range as a + // metric for converting screen pixels to meters, and use the globe's radius for converting from meters + // to arc degrees. + var canvas = this.wwd.canvas, + globe = this.wwd.globe, + globeRadius = WWMath.max(globe.equatorialRadius, globe.polarRadius), + distance = WWMath.max(1, navigator.range), + metersPerPixel = WWMath.perspectivePixelSize(canvas.clientWidth, canvas.clientHeight, distance), + forwardMeters = (ty - this.lastPoint[1]) * metersPerPixel, + sideMeters = -(tx - this.lastPoint[0]) * metersPerPixel, + forwardDegrees = forwardMeters / globeRadius * Angle.RADIANS_TO_DEGREES, + sideDegrees = sideMeters / globeRadius * Angle.RADIANS_TO_DEGREES; + + // Apply the change in latitude and longitude to this navigator, relative to the current heading. + var sinHeading = Math.sin(navigator.heading * Angle.DEGREES_TO_RADIANS), + cosHeading = Math.cos(navigator.heading * Angle.DEGREES_TO_RADIANS); + + navigator.lookAtLocation.latitude += forwardDegrees * cosHeading - sideDegrees * sinHeading; + navigator.lookAtLocation.longitude += forwardDegrees * sinHeading + sideDegrees * cosHeading; + this.lastPoint.set(tx, ty); + this.applyLimits(); + this.wwd.redraw(); + } +}; + +// Intentionally not documented. +BasicWorldWindowController.prototype.handlePanOrDrag2D = function (recognizer) { + var state = recognizer.state, + x = recognizer.clientX, + y = recognizer.clientY, + tx = recognizer.translationX, + ty = recognizer.translationY; + + var navigator = this.wwd.navigator; + if (state === WorldWind.BEGAN) { + this.beginPoint.set(x, y); + this.lastPoint.set(x, y); + } else if (state === WorldWind.CHANGED) { + var x1 = this.lastPoint[0], + y1 = this.lastPoint[1], + x2 = this.beginPoint[0] + tx, + y2 = this.beginPoint[1] + ty; + + this.lastPoint.set(x2, y2); + + var globe = this.wwd.globe, + ray = this.wwd.rayThroughScreenPoint(this.wwd.canvasCoordinates(x1, y1)), + point1 = new Vec3(0, 0, 0), + point2 = new Vec3(0, 0, 0), + origin = new Vec3(0, 0, 0); + + if (!globe.intersectsLine(ray, point1)) { + return; + } + + ray = this.wwd.rayThroughScreenPoint(this.wwd.canvasCoordinates(x2, y2)); + if (!globe.intersectsLine(ray, point2)) { + return; + } + + // Transform the original navigator state's modelview matrix to account for the gesture's change. + var modelview = Matrix.fromIdentity(); + this.wwd.computeViewingTransform(null, modelview); + modelview.multiplyByTranslation(point2[0] - point1[0], point2[1] - point1[1], point2[2] - point1[2]); + + // Compute the globe point at the screen center from the perspective of the transformed navigator state. + modelview.extractEyePoint(ray.origin); + modelview.extractForwardVector(ray.direction); + if (!globe.intersectsLine(ray, origin)) { + return; + } + + // Convert the transformed modelview matrix to a set of navigator properties, then apply those + // properties to this navigator. + var params = modelview.extractViewingParameters(origin, navigator.roll, globe, {}); + navigator.lookAtLocation.copy(params.origin); + navigator.range = params.range; + navigator.heading = params.heading; + navigator.tilt = params.tilt; + navigator.roll = params.roll; + this.applyLimits(); + this.wwd.redraw(); + } +}; + +// Intentionally not documented. +BasicWorldWindowController.prototype.handleSecondaryDrag = function (recognizer) { + var state = recognizer.state, + tx = recognizer.translationX, + ty = recognizer.translationY; + + var navigator = this.wwd.navigator; + if (state === WorldWind.BEGAN) { + this.beginHeading = navigator.heading; + this.beginTilt = navigator.tilt; + } else if (state === WorldWind.CHANGED) { + // Compute the current translation from screen coordinates to degrees. Use the canvas dimensions as a + // metric for converting the gesture translation to a fraction of an angle. + var headingDegrees = 180 * tx / this.wwd.canvas.clientWidth, + tiltDegrees = 90 * ty / this.wwd.canvas.clientHeight; + + // Apply the change in heading and tilt to this navigator's corresponding properties. + navigator.heading = this.beginHeading + headingDegrees; + navigator.tilt = this.beginTilt - tiltDegrees; + this.applyLimits(); + this.wwd.redraw(); + } +}; + +// Intentionally not documented. +BasicWorldWindowController.prototype.handlePinch = function (recognizer) { + var navigator = this.wwd.navigator; + var state = recognizer.state, + scale = recognizer.scale; + + if (state === WorldWind.BEGAN) { + this.beginRange = navigator.range; + } else if (state === WorldWind.CHANGED) { + if (scale !== 0) { + // Apply the change in pinch scale to this navigator's range, relative to the range when the gesture + // began. + navigator.range = this.beginRange / scale; + this.applyLimits(); + this.wwd.redraw(); + } + } +}; + +// Intentionally not documented. +BasicWorldWindowController.prototype.handleRotation = function (recognizer) { + var navigator = this.wwd.navigator; + var state = recognizer.state, + rotation = recognizer.rotation; + + if (state === WorldWind.BEGAN) { + this.lastRotation = 0; + } else if (state === WorldWind.CHANGED) { + // Apply the change in gesture rotation to this navigator's current heading. We apply relative to the + // current heading rather than the heading when the gesture began in order to work simultaneously with + // pan operations that also modify the current heading. + navigator.heading -= rotation - this.lastRotation; + this.lastRotation = rotation; + this.applyLimits(); + this.wwd.redraw(); + } +}; + +// Intentionally not documented. +BasicWorldWindowController.prototype.handleTilt = function (recognizer) { + var navigator = this.wwd.navigator; + var state = recognizer.state, + ty = recognizer.translationY; + + if (state === WorldWind.BEGAN) { + this.beginTilt = navigator.tilt; + } else if (state === WorldWind.CHANGED) { + // Compute the gesture translation from screen coordinates to degrees. Use the canvas dimensions as a + // metric for converting the translation to a fraction of an angle. + var tiltDegrees = -90 * ty / this.wwd.canvas.clientHeight; + // Apply the change in heading and tilt to this navigator's corresponding properties. + navigator.tilt = this.beginTilt + tiltDegrees; + this.applyLimits(); + this.wwd.redraw(); + } +}; + +// Intentionally not documented. +BasicWorldWindowController.prototype.handleWheelEvent = function (event) { + var navigator = this.wwd.navigator; + // Normalize the wheel delta based on the wheel delta mode. This produces a roughly consistent delta across + // browsers and input devices. + var normalizedDelta; + if (event.deltaMode === WheelEvent.DOM_DELTA_PIXEL) { + normalizedDelta = event.deltaY; + } else if (event.deltaMode === WheelEvent.DOM_DELTA_LINE) { + normalizedDelta = event.deltaY * 40; + } else if (event.deltaMode === WheelEvent.DOM_DELTA_PAGE) { + normalizedDelta = event.deltaY * 400; + } + + // Compute a zoom scale factor by adding a fraction of the normalized delta to 1. When multiplied by the + // navigator's range, this has the effect of zooming out or zooming in depending on whether the delta is + // positive or negative, respectfully. + var scale = 1 + normalizedDelta / 1000; + + // Apply the scale to this navigator's properties. + navigator.range *= scale; + this.applyLimits(); + this.wwd.redraw(); +}; + +// Documented in super-class. +BasicWorldWindowController.prototype.applyLimits = function () { + var navigator = this.wwd.navigator; + + // Clamp latitude to between -90 and +90, and normalize longitude to between -180 and +180. + navigator.lookAtLocation.latitude = WWMath.clamp(navigator.lookAtLocation.latitude, -90, 90); + navigator.lookAtLocation.longitude = Angle.normalizedDegreesLongitude(navigator.lookAtLocation.longitude); + + // Clamp range to values greater than 1 in order to prevent degenerating to a first-person navigator when + // range is zero. + navigator.range = WWMath.clamp(navigator.range, 1, Number.MAX_VALUE); + + // Normalize heading to between -180 and +180. + navigator.heading = Angle.normalizedDegrees(navigator.heading); + + // Clamp tilt to between 0 and +90 to prevent the viewer from going upside down. + navigator.tilt = WWMath.clamp(navigator.tilt, 0, 90); + + // Normalize heading to between -180 and +180. + navigator.roll = Angle.normalizedDegrees(navigator.roll); + + // Apply 2D limits when the globe is 2D. + if (this.wwd.globe.is2D() && navigator.enable2DLimits) { + // Clamp range to prevent more than 360 degrees of visible longitude. Assumes a 45 degree horizontal + // field of view. + var maxRange = 2 * Math.PI * this.wwd.globe.equatorialRadius; + navigator.range = WWMath.clamp(navigator.range, 1, maxRange); + + // Force tilt to 0 when in 2D mode to keep the viewer looking straight down. + navigator.tilt = 0; + } +}; + +export default BasicWorldWindowController; + diff --git a/web/test/WebWorldWind/src/README.md b/web/test/WebWorldWind/src/README.md new file mode 100644 index 00000000..7208f1d4 --- /dev/null +++ b/web/test/WebWorldWind/src/README.md @@ -0,0 +1,7 @@ +# WebWorldWind + +This is a project from: https://github.com/NASAWorldWind/WebWorldWind + +# License + +Apache \ No newline at end of file diff --git a/web/test/WebWorldWind/src/WorldWind.js b/web/test/WebWorldWind/src/WorldWind.js new file mode 100644 index 00000000..6eef4997 --- /dev/null +++ b/web/test/WebWorldWind/src/WorldWind.js @@ -0,0 +1,517 @@ +/* + * 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 AbstractError from './error/AbstractError'; +import Annotation from './shapes/Annotation'; +import AnnotationAttributes from './shapes/AnnotationAttributes'; +import ArgumentError from './error/ArgumentError'; +import AsterV2ElevationCoverage from './globe/AsterV2ElevationCoverage'; +import AtmosphereLayer from './layer/AtmosphereLayer'; +import AtmosphereProgram from './shaders/AtmosphereProgram'; +import BasicProgram from './shaders/BasicProgram'; +import BasicTextureProgram from './shaders/BasicTextureProgram'; +import BasicWorldWindowController from './BasicWorldWindowController'; +import BoundingBox from './geom/BoundingBox'; +import ClickRecognizer from './gesture/ClickRecognizer'; +import Color from './util/Color'; +import Compass from './shapes/Compass'; +import DragRecognizer from './gesture/DragRecognizer'; +import DrawContext from './render/DrawContext'; +import EarthElevationModel from './globe/EarthElevationModel'; +import ElevationCoverage from './globe/ElevationCoverage'; +import ElevationModel from './globe/ElevationModel'; +import Font from './util/Font'; +import FrameStatistics from './util/FrameStatistics'; +import FramebufferTexture from './render/FramebufferTexture'; +import FramebufferTile from './render/FramebufferTile'; +import FramebufferTileController from './render/FramebufferTileController'; +import Frustum from './geom/Frustum'; +import GebcoElevationCoverage from './globe/GebcoElevationCoverage'; +import GeographicMesh from './shapes/GeographicMesh'; +import GeographicProjection from './projections/GeographicProjection'; +import GeographicText from './shapes/GeographicText'; +import GestureRecognizer from './gesture/GestureRecognizer'; +import Globe from './globe/Globe'; +import GpuProgram from './shaders/GpuProgram'; +import GpuResourceCache from './cache/GpuResourceCache'; +import GpuShader from './shaders/GpuShader'; +import GroundProgram from './shaders/GroundProgram'; +import HashMap from './util/HashMap'; +import ImageSource from './util/ImageSource'; +import ImageTile from './render/ImageTile'; +import Insets from './util/Insets'; +import Layer from './layer/Layer'; +import Level from './util/Level'; +import LevelSet from './util/LevelSet'; +import Line from './geom/Line'; +import Location from './geom/Location'; +import LookAtNavigator from './navigate/LookAtNavigator'; +import Matrix from './geom/Matrix'; +import MemoryCache from './cache/MemoryCache'; +import MemoryCacheListener from './cache/MemoryCacheListener'; +import MercatorTiledImageLayer from './layer/MercatorTiledImageLayer'; +import Navigator from './navigate/Navigator'; +import NotYetImplementedError from './error/NotYetImplementedError'; +import Offset from './util/Offset'; +import PanRecognizer from './gesture/PanRecognizer'; +import Path from './shapes/Path'; +import PickedObject from './pick/PickedObject'; +import PickedObjectList from './pick/PickedObjectList'; +import PinchRecognizer from './gesture/PinchRecognizer'; +import Placemark from './shapes/Placemark'; +import PlacemarkAttributes from './shapes/PlacemarkAttributes'; +import Plane from './geom/Plane'; +import Polygon from './shapes/Polygon'; +import Position from './geom/Position'; +import ProjectionWgs84 from './projections/ProjectionWgs84'; +import Rectangle from './geom/Rectangle'; +import Renderable from './render/Renderable'; +import RotationRecognizer from './gesture/RotationRecognizer'; +import ScreenImage from './shapes/ScreenImage'; +import ScreenText from './shapes/ScreenText'; +import Sector from './geom/Sector'; +import ShapeAttributes from './shapes/ShapeAttributes'; +import SkyProgram from './shaders/SkyProgram'; +import StarFieldLayer from './layer/StarFieldLayer'; +import StarFieldProgram from './shaders/StarFieldProgram'; +import SurfaceImage from './shapes/SurfaceImage'; +import SurfaceCircle from './shapes/SurfaceCircle'; +import SurfaceEllipse from './shapes/SurfaceEllipse'; +import SurfacePolygon from './shapes/SurfacePolygon'; +import SurfacePolyline from './shapes/SurfacePolyline'; +import SurfaceRectangle from './shapes/SurfaceRectangle'; +import SurfaceRenderable from './render/SurfaceRenderable'; +import SurfaceSector from './shapes/SurfaceSector'; +import SurfaceShape from './shapes/SurfaceShape'; +import SurfaceShapeTile from './shapes/SurfaceShapeTile'; +import SurfaceShapeTileBuilder from './shapes/SurfaceShapeTileBuilder'; +import SurfaceTile from './render/SurfaceTile'; +import SurfaceTileRenderer from './render/SurfaceTileRenderer'; +import SurfaceTileRendererProgram from './shaders/SurfaceTileRendererProgram'; +import TapRecognizer from './gesture/TapRecognizer'; +import Terrain from './globe/Terrain'; +import TerrainTile from './globe/TerrainTile'; +import TerrainTileList from './globe/TerrainTileList'; +import Tessellator from './globe/Tessellator'; +import Text from './shapes/Text'; +import TextAttributes from './shapes/TextAttributes'; +import TextRenderer from './render/TextRenderer'; +import Texture from './render/Texture'; +import TextureTile from './render/TextureTile'; +import Tile from './util/Tile'; +import TiledElevationCoverage from './globe/TiledElevationCoverage'; +import TiledImageLayer from './layer/TiledImageLayer'; +import TileFactory from './util/TileFactory'; +import TiltRecognizer from './gesture/TiltRecognizer'; +import Touch from './gesture/Touch'; +import TriangleMesh from './shapes/TriangleMesh'; +import UsgsNedElevationCoverage from './error/UnsupportedOperationError'; +import UsgsNedHiElevationCoverage from './globe/UsgsNedElevationCoverage'; +import UnsupportedOperationError from './globe/UsgsNedHiElevationCoverage'; +import UrlBuilder from './util/UrlBuilder'; +import Vec2 from './geom/Vec2'; +import Vec3 from './geom/Vec3'; +import WmsUrlBuilder from './util/WmsUrlBuilder'; +import WorldWindow from './WorldWindow'; +import WorldWindowController from './WorldWindowController'; +import WWUtil from './util/WWUtil'; +import XYZLayer from './layer/XYZLayer'; +import WWMath from './util/WWMath'; + +/** + * This is the top-level WorldWind module. It is global. + * @exports WorldWind + * @global + */ +var WorldWind = { + /** + * The WorldWind version number. + * @default "0.9.0" + * @constant + */ + VERSION: "0.9.0", + + // PLEASE KEEP THE ENTRIES BELOW IN ALPHABETICAL ORDER + /** + * Indicates an altitude mode relative to the globe's ellipsoid. + * @constant + */ + ABSOLUTE: "absolute", + + /** + * Indicates that a redraw callback has been called immediately after a redraw. + * @constant + */ + AFTER_REDRAW: "afterRedraw", + + /** + * Indicates that a redraw callback has been called immediately before a redraw. + * @constant + */ + BEFORE_REDRAW: "beforeRedraw", + + /** + * The BEGAN gesture recognizer state. Continuous gesture recognizers transition to this state from the + * POSSIBLE state when the gesture is first recognized. + * @constant + */ + BEGAN: "began", + + /** + * The CANCELLED gesture recognizer state. Continuous gesture recognizers may transition to this state from + * the BEGAN state or the CHANGED state when the touch events are cancelled. + * @constant + */ + CANCELLED: "cancelled", + + /** + * The CHANGED gesture recognizer state. Continuous gesture recognizers transition to this state from the + * BEGAN state or the CHANGED state, whenever an input event indicates a change in the gesture. + * @constant + */ + CHANGED: "changed", + + /** + * Indicates an altitude mode always on the terrain. + * @constant + */ + CLAMP_TO_GROUND: "clampToGround", + + /** + * The radius of Earth. + * @constant + * @deprecated Use WGS84_SEMI_MAJOR_AXIS instead. + */ + EARTH_RADIUS: 6371e3, + + /** + * Indicates the cardinal direction east. + * @constant + */ + EAST: "east", + + /** + * The ENDED gesture recognizer state. Continuous gesture recognizers transition to this state from either + * the BEGAN state or the CHANGED state when the current input no longer represents the gesture. + * @constant + */ + ENDED: "ended", + + /** + * The FAILED gesture recognizer state. Gesture recognizers transition to this state from the POSSIBLE state + * when the gesture cannot be recognized given the current input. + * @constant + */ + FAILED: "failed", + + /** + * Indicates a linear filter. + * @constant + */ + FILTER_LINEAR: "filter_linear", + + /** + * Indicates a nearest neighbor filter. + * @constant + */ + FILTER_NEAREST: "filter_nearest", + + /** + * Indicates a great circle path. + * @constant + */ + GREAT_CIRCLE: "greatCircle", + + /** + * Indicates a linear, straight line path. + * @constant + */ + LINEAR: "linear", + + /** + * Indicates a multi-point shape, typically within a shapefile. + */ + MULTI_POINT: "multiPoint", + + /** + * Indicates the cardinal direction north. + * @constant + */ + NORTH: "north", + + /** + * Indicates a null shape, typically within a shapefile. + * @constant + */ + NULL: "null", + + /** + * Indicates that the associated parameters are fractional values of the virtual rectangle's width or + * height in the range [0, 1], where 0 indicates the rectangle's origin and 1 indicates the corner + * opposite its origin. + * @constant + */ + OFFSET_FRACTION: "fraction", + + /** + * Indicates that the associated parameters are in units of pixels relative to the virtual rectangle's + * corner opposite its origin corner. + * @constant + */ + OFFSET_INSET_PIXELS: "insetPixels", + + /** + * Indicates that the associated parameters are in units of pixels relative to the virtual rectangle's + * origin. + * @constant + */ + OFFSET_PIXELS: "pixels", + + /** + * Indicates a point shape, typically within a shapefile. + */ + POINT: "point", + + /** + * Indicates a polyline shape, typically within a shapefile. + */ + POLYLINE: "polyline", + + /** + * Indicates a polygon shape, typically within a shapefile. + */ + POLYGON: "polygon", + + /** + * The POSSIBLE gesture recognizer state. Gesture recognizers in this state are idle when there is no input + * event to evaluate, or are evaluating input events to determine whether or not to transition into another + * state. + * @constant + */ + POSSIBLE: "possible", + + /** + * The RECOGNIZED gesture recognizer state. Discrete gesture recognizers transition to this state from the + * POSSIBLE state when the gesture is recognized. + * @constant + */ + RECOGNIZED: "recognized", + + /** + * The event name of WorldWind redraw events. + */ + REDRAW_EVENT_TYPE: "WorldWindRedraw", + + /** + * Indicates that the related value is specified relative to the globe. + * @constant + */ + RELATIVE_TO_GLOBE: "relativeToGlobe", + + /** + * Indicates an altitude mode relative to the terrain. + * @constant + */ + RELATIVE_TO_GROUND: "relativeToGround", + + /** + * Indicates that the related value is specified relative to the plane of the screen. + * @constant + */ + RELATIVE_TO_SCREEN: "relativeToScreen", + + /** + * Indicates a rhumb path -- a path of constant bearing. + * @constant + */ + RHUMB_LINE: "rhumbLine", + + /** + * Indicates the cardinal direction south. + * @constant + */ + SOUTH: "south", + + /** + * Indicates the cardinal direction west. + * @constant + */ + WEST: "west", + + /** + * WGS 84 reference value for Earth's semi-major axis: 6378137.0. Taken from NGA.STND.0036_1.0.0_WGS84, + * section 3.4.1. + * @constant + */ + WGS84_SEMI_MAJOR_AXIS: 6378137.0, + + /** + * WGS 84 reference value for Earth's inverse flattening: 298.257223563. Taken from + * NGA.STND.0036_1.0.0_WGS84, section 3.4.2. + * @constant + */ + WGS84_INVERSE_FLATTENING: 298.257223563 +}; + +WorldWind['AbstractError'] = AbstractError; +WorldWind['Annotation'] = Annotation; +WorldWind['AnnotationAttributes'] = AnnotationAttributes; +WorldWind['ArgumentError'] = ArgumentError; +WorldWind['AsterV2ElevationCoverage'] = AsterV2ElevationCoverage; +WorldWind['AtmosphereLayer'] = AtmosphereLayer; +WorldWind['AtmosphereProgram'] = AtmosphereProgram; +WorldWind['BasicProgram'] = BasicProgram; +WorldWind['BasicTextureProgram'] = BasicTextureProgram; +WorldWind['BasicWorldWindowController'] = BasicWorldWindowController; +WorldWind['BoundingBox'] = BoundingBox; +WorldWind['ClickRecognizer'] = ClickRecognizer; +WorldWind['Color'] = Color; +WorldWind['Compass'] = Compass; +WorldWind['DragRecognizer'] = DragRecognizer; +WorldWind['DrawContext'] = DrawContext; +WorldWind['EarthElevationModel'] = EarthElevationModel; +WorldWind['ElevationCoverage'] = ElevationCoverage; +WorldWind['ElevationModel'] = ElevationModel; +WorldWind['Font'] = Font; +WorldWind['FrameStatistics'] = FrameStatistics; +WorldWind['FramebufferTexture'] = FramebufferTexture; +WorldWind['FramebufferTile'] = FramebufferTile; +WorldWind['FramebufferTileController'] = FramebufferTileController; +WorldWind['Frustum'] = Frustum; +WorldWind['GebcoElevationCoverage'] = GebcoElevationCoverage; +WorldWind['GeographicMesh'] = GeographicMesh; +WorldWind['GeographicProjection'] = GeographicProjection; +WorldWind['GeographicText'] = GeographicText; +WorldWind['GestureRecognizer'] = GestureRecognizer; +WorldWind['Globe'] = Globe; +WorldWind['GpuProgram'] = GpuProgram; +WorldWind['GpuResourceCache'] = GpuResourceCache; +WorldWind['GpuShader'] = GpuShader; +WorldWind['GroundProgram'] = GroundProgram; +WorldWind['HashMap'] = HashMap; +WorldWind['ImageSource'] = ImageSource; +WorldWind['ImageTile'] = ImageTile; +WorldWind['Insets'] = Insets; +WorldWind['Layer'] = Layer; +WorldWind['Level'] = Level; +WorldWind['LevelSet'] = LevelSet; +WorldWind['Line'] = Line; +WorldWind['Location'] = Location; +WorldWind['LookAtNavigator'] = LookAtNavigator; +WorldWind['Matrix'] = Matrix; +WorldWind['MemoryCache'] = MemoryCache; +WorldWind['MemoryCacheListener'] = MemoryCacheListener; +WorldWind['MercatorTiledImageLayer'] = MercatorTiledImageLayer; +WorldWind['Navigator'] = Navigator; +WorldWind['NotYetImplementedError'] = NotYetImplementedError; +WorldWind['Offset'] = Offset; +WorldWind['PanRecognizer'] = PanRecognizer; +WorldWind['Path'] = Path; +WorldWind['PickedObject'] = PickedObject; +WorldWind['PickedObjectList'] = PickedObjectList; +WorldWind['PinchRecognizer'] = PinchRecognizer; +WorldWind['Placemark'] = Placemark; +WorldWind['PlacemarkAttributes'] = PlacemarkAttributes; +WorldWind['Plane'] = Plane; +WorldWind['Polygon'] = Polygon; +WorldWind['Position'] = Position; +WorldWind['ProjectionWgs84'] = ProjectionWgs84; +WorldWind['Rectangle'] = Rectangle; +WorldWind['Renderable'] = Renderable; +WorldWind['RotationRecognizer'] = RotationRecognizer; +WorldWind['ScreenText'] = ScreenText; +WorldWind['ScreenImage'] = ScreenImage; +WorldWind['Sector'] = Sector; +WorldWind['ShapeAttributes'] = ShapeAttributes; +WorldWind['SkyProgram'] = SkyProgram; +WorldWind['StarFieldLayer'] = StarFieldLayer; +WorldWind['StarFieldProgram'] = StarFieldProgram; +WorldWind['SurfaceImage'] = SurfaceImage; +WorldWind['SurfaceCircle'] = SurfaceCircle; +WorldWind['SurfaceEllipse'] = SurfaceEllipse; +WorldWind['SurfacePolygon'] = SurfacePolygon; +WorldWind['SurfacePolyline'] = SurfacePolyline; +WorldWind['SurfaceRectangle'] = SurfaceRectangle; +WorldWind['SurfaceRenderable'] = SurfaceRenderable; +WorldWind['SurfaceSector'] = SurfaceSector; +WorldWind['SurfaceShape'] = SurfaceShape; +WorldWind['SurfaceShapeTile'] = SurfaceShapeTile; +WorldWind['SurfaceShapeTileBuilder'] = SurfaceShapeTileBuilder; +WorldWind['SurfaceTile'] = SurfaceTile; +WorldWind['SurfaceTileRenderer'] = SurfaceTileRenderer; +WorldWind['SurfaceTileRendererProgram'] = SurfaceTileRendererProgram; +WorldWind['TapRecognizer'] = TapRecognizer; +WorldWind['Terrain'] = Terrain; +WorldWind['TerrainTile'] = TerrainTile; +WorldWind['TerrainTileList'] = TerrainTileList; +WorldWind['Tessellator'] = Tessellator; +WorldWind['Text'] = Text; +WorldWind['TextAttributes'] = TextAttributes; +WorldWind['TextRenderer'] = TextRenderer; +WorldWind['Texture'] = Texture; +WorldWind['TextureTile'] = TextureTile; +WorldWind['Tile'] = Tile; +WorldWind['TiledElevationCoverage'] = TiledElevationCoverage; +WorldWind['TiledImageLayer'] = TiledImageLayer; +WorldWind['TileFactory'] = TileFactory; +WorldWind['TiltRecognizer'] = TiltRecognizer; +WorldWind['Touch'] = Touch; +WorldWind['TriangleMesh'] = TriangleMesh; +WorldWind['UsgsNedElevationCoverage'] = UsgsNedElevationCoverage; +WorldWind['UsgsNedHiElevationCoverage'] = UsgsNedHiElevationCoverage; +WorldWind['UnsupportedOperationError'] = UnsupportedOperationError; +WorldWind['UrlBuilder'] = UrlBuilder; +WorldWind['Vec2'] = Vec2; +WorldWind['Vec3'] = Vec3; +WorldWind['WmsUrlBuilder'] = WmsUrlBuilder; +WorldWind['WorldWindow'] = WorldWindow; +WorldWind['WorldWindowController'] = WorldWindowController; +WorldWind['XYZLayer'] = XYZLayer; +WorldWind['WWMath'] = WWMath; + +/** + * Holds configuration parameters for WorldWind. Applications may modify these parameters prior to creating + * their first WorldWind objects. Configuration properties are: + * + * @type {{gpuCacheSize: number}} + */ +WorldWind.configuration = { + gpuCacheSize: 250e6, + baseUrl: WWUtil.worldwindlibLocation() || WWUtil.currentUrlSansFilePart() + '/', + layerRetrievalQueueSize: 16, + coverageRetrievalQueueSize: 16, + bingLogoPlacement: new Offset(WorldWind.OFFSET_INSET_PIXELS, 7, WorldWind.OFFSET_PIXELS, 7), + bingLogoAlignment: new Offset(WorldWind.OFFSET_FRACTION, 1, WorldWind.OFFSET_FRACTION, 0) +}; + +/** + * Indicates the Bing Maps key to use when requesting Bing Maps resources. + * @type {String} + * @default null + */ +WorldWind.BingMapsKey = null; + +window.WorldWind = WorldWind; + +export default WorldWind; diff --git a/web/test/WebWorldWind/src/WorldWindow.js b/web/test/WebWorldWind/src/WorldWindow.js new file mode 100644 index 00000000..a75bcc49 --- /dev/null +++ b/web/test/WebWorldWind/src/WorldWindow.js @@ -0,0 +1,1521 @@ +/* + * 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 WorldWindow + */ +import ArgumentError from './error/ArgumentError'; +import BasicWorldWindowController from './BasicWorldWindowController'; +import DrawContext from './render/DrawContext'; +import EarthElevationModel from './globe/EarthElevationModel'; +import FrameStatistics from './util/FrameStatistics'; +import Frustum from './geom/Frustum'; +import Globe from './globe/Globe'; +import GoToAnimator from './util/GoToAnimator'; +import Line from './geom/Line'; +import Logger from './util/Logger'; +import LookAtNavigator from './navigate/LookAtNavigator'; +import Matrix from './geom/Matrix'; +import PickedObjectList from './pick/PickedObjectList'; +import Position from './geom/Position'; +import Rectangle from './geom/Rectangle'; +import SurfaceShape from './shapes/SurfaceShape'; +import Vec2 from './geom/Vec2'; +import Vec3 from './geom/Vec3'; +import WWMath from './util/WWMath'; + + +/** + * Constructs a WorldWind window for an HTML canvas. + * @alias WorldWindow + * @constructor + * @classdesc Represents a WorldWind window for an HTML canvas. + * @param {String|HTMLCanvasElement} canvasElem The ID assigned to the HTML canvas in the document or the canvas + * element itself. + * @param {ElevationModel} elevationModel An optional argument indicating the elevation model to use for the World + * Window. If missing or null, a default elevation model is used. + * @throws {ArgumentError} If there is no HTML element with the specified ID in the document, or if the + * HTML canvas does not support WebGL. + */ +function WorldWindow(canvasElem, elevationModel) { + if (!window.WebGLRenderingContext) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "constructor", "webglNotSupported")); + } + + // Get the actual canvas element either directly or by ID. + var canvas; + if (canvasElem instanceof HTMLCanvasElement) { + canvas = canvasElem; + } else { + // Attempt to get the HTML canvas with the specified ID. + canvas = document.getElementById(canvasElem); + + if (!canvas) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "constructor", + "The specified canvas ID is not in the document.")); + } + } + + // Create the WebGL context associated with the HTML canvas. + var gl = this.createContext(canvas); + if (!gl) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "constructor", "webglNotSupported")); + } + + + // Internal. Intentionally not documented. + this.drawContext = new DrawContext(gl); + + // Internal. Intentionally not documented. Must be initialized before the navigator is created. + this.eventListeners = {}; + + // Internal. Intentionally not documented. Initially true in order to redraw at least once. + this.redrawRequested = true; + + // Internal. Intentionally not documented. + this.redrawRequestId = null; + + // Internal. Intentionally not documented. + this.scratchModelview = Matrix.fromIdentity(); + + // Internal. Intentionally not documented. + this.scratchProjection = Matrix.fromIdentity(); + + // Internal. Intentionally not documented. + this.hasStencilBuffer = gl.getContextAttributes().stencil; + + /** + * The HTML canvas associated with this WorldWindow. + * @type {HTMLElement} + * @readonly + */ + this.canvas = canvas; + + /** + * The number of bits in the depth buffer associated with this WorldWindow. + * @type {number} + * @readonly + */ + this.depthBits = gl.getParameter(gl.DEPTH_BITS); + + /** + * The current viewport of this WorldWindow. + * @type {Rectangle} + * @readonly + */ + this.viewport = new Rectangle(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); + + /** + * The globe displayed. + * @type {Globe} + */ + this.globe = new Globe(new EarthElevationModel()); + + /** + * The layers to display in this WorldWindow. + * This property is read-only. Use [addLayer]{@link WorldWindow#addLayer} or + * [insertLayer]{@link WorldWindow#insertLayer} to add layers to this WorldWindow. + * Use [removeLayer]{@link WorldWindow#removeLayer} to remove layers from this WorldWindow. + * @type {Layer[]} + * @readonly + */ + this.layers = []; + + /** + * The navigator used to manipulate the globe. + * @type {LookAtNavigator} + * @default [LookAtNavigator]{@link LookAtNavigator} + */ + this.navigator = new LookAtNavigator(); + + /** + * The controller used to manipulate the globe. + * @type {WorldWindowController} + * @default [BasicWorldWindowController]{@link BasicWorldWindowController} + */ + this.worldWindowController = new BasicWorldWindowController(this); + + /** + * The vertical exaggeration to apply to the terrain. + * @type {Number} + */ + this.verticalExaggeration = 1; + + /** + * 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 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 whether this WorldWindow should be configured for sub-surface rendering. If true, shapes + * below the terrain can be seen when the terrain is made transparent. If false, sub-surface shapes are + * not visible, however, performance is slightly increased. + * @type {boolean} + * @default false + */ + this.subsurfaceMode = false; + + /** + * The opacity to apply to terrain and surface shapes. This property is typically used when viewing + * the sub-surface. It modifies the opacity of the terrain and surface shapes as a whole. It should be + * a number between 0 and 1. It is compounded with the individual opacities of the image layers and + * surface shapes on the terrain. + * @type {Number} + * @default 1 + */ + this.surfaceOpacity = 1; + + /** + * Performance statistics for this WorldWindow. + * @type {FrameStatistics} + */ + this.frameStatistics = new FrameStatistics(); + + /** + * The {@link GoToAnimator} used by this WorldWindow to respond to its goTo method. + * @type {GoToAnimator} + */ + this.goToAnimator = new GoToAnimator(this); + + // Documented with its property accessor below. + this._redrawCallbacks = []; + + // Documented with its property accessor below. + this._orderedRenderingFilters = [ + function (dc) { + thisWindow.declutter(dc, 1); + }, + function (dc) { + thisWindow.declutter(dc, 2); + } + ]; + + // Intentionally not documented. + this.pixelScale = 1; + + // Prevent the browser's default actions in response to mouse and touch events, which interfere with + // navigation. Register these event listeners before any others to ensure that they're called last. + function preventDefaultListener(event) { + event.preventDefault(); + } + + this.addEventListener("mousedown", preventDefaultListener); + this.addEventListener("touchstart", preventDefaultListener); + this.addEventListener("contextmenu", preventDefaultListener); + this.addEventListener("wheel", preventDefaultListener); + + var thisWindow = this; + + // Redirect various UI interactions to the appropriate handler. + function onGestureEvent(event) { + thisWindow.onGestureEvent(event); + } + + if (window.PointerEvent) { + // Prevent the browser's default actions in response to pointer events which interfere with navigation. + // This CSS style property is configured here to ensure that it's set for all applications. + this.canvas.style.setProperty("touch-action", "none"); + + this.addEventListener("pointerdown", onGestureEvent, false); + window.addEventListener("pointermove", onGestureEvent, false); // get pointermove events outside event target + window.addEventListener("pointercancel", onGestureEvent, false); // get pointercancel events outside event target + window.addEventListener("pointerup", onGestureEvent, false); // get pointerup events outside event target + } else { + this.addEventListener("mousedown", onGestureEvent, false); + window.addEventListener("mousemove", onGestureEvent, false); // get mousemove events outside event target + window.addEventListener("mouseup", onGestureEvent, false); // get mouseup events outside event target + this.addEventListener("touchstart", onGestureEvent, false); + this.addEventListener("touchmove", onGestureEvent, false); + this.addEventListener("touchend", onGestureEvent, false); + this.addEventListener("touchcancel", onGestureEvent, false); + } + + // Register wheel event listeners on the WorldWindow's canvas. + this.addEventListener("wheel", function (event) { + onGestureEvent(event); + }); + + // Set up to handle WebGL context lost events. + function handleContextLost(event) { + thisWindow.handleContextLost(event); + } + + this.canvas.addEventListener("webglcontextlost", handleContextLost, false); + + // Set up to handle WebGL context restored events. + function handleContextRestored(event) { + thisWindow.handleContextRestored(event); + } + + this.canvas.addEventListener("webglcontextrestored", handleContextRestored, false); + + // Set up to handle WebGL context events and WorldWind redraw request events. Imagery uses the canvas + // redraw events because images are generally specific to the WebGL context associated with the canvas. + // Elevation models use the global window redraw events because they can be shared among WorldWindows. + function handleRedrawEvent(event) { + thisWindow.handleRedrawEvent(event); + } + + this.canvas.addEventListener(WorldWind.REDRAW_EVENT_TYPE, handleRedrawEvent, false); + window.addEventListener(WorldWind.REDRAW_EVENT_TYPE, handleRedrawEvent, false); + + // Render to the WebGL context in an animation frame loop until the WebGL context is lost. + this.animationFrameLoop(); +} + +Object.defineProperties(WorldWindow.prototype, { + /** + * An array of functions to call during ordered rendering prior to rendering the ordered renderables. + * Each function is passed one argument, the current draw context. The function may modify the + * ordered renderables in the draw context's ordered renderable list, which has been sorted from front + * to back when the filter function is called. Ordered rendering filters are typically used to apply + * decluttering. The default set of filter functions contains one function that declutters shapes with + * declutter group ID of 1 ({@link GeographicText} by default) and one function that declutters shapes + * with declutter group ID 2 ({@link Placemark} by default). Applications can add functions to this + * array or remove them. + * @type {Function[]} + * @default [WorldWindow.declutter]{@link WorldWindow#declutter} with a group ID of 1 + * @readonly + * @memberof WorldWindow.prototype + */ + orderedRenderingFilters: { + get: function () { + return this._orderedRenderingFilters; + } + }, + /** + * The list of callbacks to call immediately before and immediately after performing a redraw. The callbacks + * have two arguments: this WorldWindow and the redraw stage, e.g., 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: + *

+ * @param {Vec3} origin The origin of the viewing parameters, in model coordinates. + * @param {Number} roll The view's roll, in degrees. + * @param {Globe} globe The globe the viewer is looking at. + * @param {Object} result A pre-allocated object in which to return the viewing parameters. + * + * @return {Object} The specified result argument containing a parameterization of this viewing matrix. + * + * @throws {ArgumentError} If either the specified origin or globe are null or undefined or the specified + * result argument is null or undefined. + */ +Matrix.prototype.extractViewingParameters = function (origin, roll, globe, result) { + if (!origin) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "extractViewingParameters", + "The specified origin is null or undefined.")); + } + + if (!globe) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "extractViewingParameters", "missingGlobe")); + } + + if (!result) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "extractViewingParameters", "missingResult")); + } + + var originPos = new Position(0, 0, 0), + modelviewLocal = Matrix.fromIdentity(), + range, + ct, + st, + tilt, + cr, sr, + ch, sh, + heading; + + globe.computePositionFromPoint(origin[0], origin[1], origin[2], originPos); + + // Transform the modelview matrix to a local coordinate system at the origin. This eliminates the geographic + // transform contained in the modelview matrix while maintaining rotation and translation relative to the origin. + modelviewLocal.copy(this); + modelviewLocal.multiplyByLocalCoordinateTransform(origin, globe); + + range = -modelviewLocal[11]; + ct = modelviewLocal[10]; + st = Math.sqrt(modelviewLocal[2] * modelviewLocal[2] + modelviewLocal[6] * modelviewLocal[6]); + tilt = Math.atan2(st, ct) * Angle.RADIANS_TO_DEGREES; + + cr = Math.cos(roll * Angle.DEGREES_TO_RADIANS); + sr = Math.sin(roll * Angle.DEGREES_TO_RADIANS); + ch = cr * modelviewLocal[0] - sr * modelviewLocal[4]; + sh = sr * modelviewLocal[5] - cr * modelviewLocal[1]; + heading = Math.atan2(sh, ch) * Angle.RADIANS_TO_DEGREES; + + result['origin'] = originPos; + result['range'] = range; + result['heading'] = heading; + result['tilt'] = tilt; + result['roll'] = roll; + + return result; +}; + +/** + * Applies a specified depth offset to this projection matrix. + *

+ * 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: + *