From 264b0210332193486855e249640afac11178357c Mon Sep 17 00:00:00 2001 From: Eugene Maksymenko Date: Mon, 7 Dec 2020 21:24:06 +0200 Subject: [PATCH 1/6] Added keyboard navigation capabilities to the globe (#4) * Added the KeyboardControls module to utils. * Added the creation of a KeyboardControls object to the WorldWindow. --- src/WorldWindow.js | 8 ++ src/util/KeyboardControls.js | 213 +++++++++++++++++++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 src/util/KeyboardControls.js diff --git a/src/WorldWindow.js b/src/WorldWindow.js index 3754f6609..c77a9c50c 100644 --- a/src/WorldWindow.js +++ b/src/WorldWindow.js @@ -39,6 +39,7 @@ define([ './globe/Globe2D', './util/GoToAnimator', './cache/GpuResourceCache', + './util/KeyboardControls', './geom/Line', './util/Logger', './navigate/LookAtNavigator', @@ -64,6 +65,7 @@ define([ Globe2D, GoToAnimator, GpuResourceCache, + KeyboardControls, Line, Logger, LookAtNavigator, @@ -192,6 +194,12 @@ define([ */ this.worldWindowController = new BasicWorldWindowController(this); + /** + * The controller used to manipulate the globe with the keyboard. + * @type {KeyboardController} + */ + this.keyboardControls = new KeyboardControls(this); + /** * The vertical exaggeration to apply to the terrain. * @type {Number} diff --git a/src/util/KeyboardControls.js b/src/util/KeyboardControls.js new file mode 100644 index 000000000..fe2090988 --- /dev/null +++ b/src/util/KeyboardControls.js @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2016, 2018 Bruce Schubert. + * The MIT License + * http://www.opensource.org/licenses/mit-license + */ + +/** + * The KeyboardControls module provides keyboard controls for the globe. + * Note: the canvas must be focusable; this can be accomplished by establishing the "tabindex" + * on the canvas element. + * + * @returns {KeyboardControls} + * + * @@author Bruce Schubert + */ +define([ + '../geom/Location'], + function ( + Location) { + "use strict"; + /** + * Creates a KeyboardController that dispatches keystrokes from the + * WorldWindow to the Navigator. Note: the WorldWindow's canvas must be focusable; + * this can be accomplished by establishing the "tabindex" on the canvas element. + * @param {WorldWindow} wwd The keyboard event generator. + * @returns {KeyboardControls} + */ + var KeyboardControls = function (wwd) { + this.wwd = wwd; + this.enabled = true; + + // The tabindex must be set for the keyboard controls to work + var tabIndex = this.wwd.canvas.tabIndex; + if (typeof tabIndex === 'undefined' || tabIndex < 0) { + this.wwd.canvas.tabIndex = 0; + } + + var self = this; + this.wwd.addEventListener('keydown', function (event) { + self.handleKeyDown(event); + }); + this.wwd.addEventListener('keyup', function (event) { + self.handleKeyUp(event); + }); + // Ensure keyboard controls are operational by setting the focus to the canvas + this.wwd.addEventListener("click", function (event) { + if (self.enabled) { + self.wwd.canvas.focus(); + } + }); + + /** + * The incremental amount to increase or decrease the eye distance (for zoom) each cycle. + * @type {Number} + */ + this.zoomIncrement = 0.01; + + /** + * The scale factor governing the pan speed. Increased values cause faster panning. + * @type {Number} + */ + this.panIncrement = 0.0000000005; + + }; + + /** + * Controls the globe with the keyboard. + * @param {KeyboardEvent} event + */ + KeyboardControls.prototype.handleKeyDown = function (event) { + + if (!this.enabled) { + return; + } + + // TODO: find a way to make this code portable for different keyboard layouts + if (event.keyCode === 187 || event.keyCode === 61) { // + key || +/= key + this.handleZoom("zoomIn"); + event.preventDefault(); + } + else if (event.keyCode === 189 || event.keyCode === 173) { // - key || _/- key + this.handleZoom("zoomOut"); + event.preventDefault(); + } + else if (event.keyCode === 37) { // Left arrow + this.handlePan("panLeft"); + event.preventDefault(); + } + else if (event.keyCode === 38) { // Up arrow + this.handlePan("panUp"); + event.preventDefault(); + } + else if (event.keyCode === 39) { // Right arrow + this.handlePan("panRight"); + event.preventDefault(); + } + else if (event.keyCode === 40) { // Down arrow + this.handlePan("panDown"); + event.preventDefault(); + } + else if (event.keyCode === 78) { // N key + this.resetHeading(); + event.preventDefault(); + } + else if (event.keyCode === 82) { // R key + this.resetHeadingAndTilt(); + event.preventDefault(); + } + }; + + /** + * Reset the view to North up. + */ + KeyboardControls.prototype.resetHeading = function () { + this.wwd.navigator.heading = Number(0); + this.wwd.redraw(); + }; + + /** + * Reset the view to North up and nadir. + */ + KeyboardControls.prototype.resetHeadingAndTilt = function () { + this.wwd.navigator.heading = 0; + this.wwd.navigator.tilt = 0; + this.wwd.redraw(); // calls applyLimits which may change the location + +// // Tilting the view will change the location due to a deficiency in +// // the early release of WW. So we set the location to the center of the +// // current crosshairs position (viewpoint) to resolve this issue +// var viewpoint = this.getViewpoint(), +// lat = viewpoint.target.latitude, +// lon = viewpoint.target.longitude; +// this.lookAt(lat, lon); + }; + + /** + * + * @param {KeyupEvent} event + */ + KeyboardControls.prototype.handleKeyUp = function (event) { + if (this.activeOperation) { + this.activeOperation = null; + event.preventDefault(); + } + }; + + /** + * + * @param {type} operation + */ + KeyboardControls.prototype.handleZoom = function (operation) { + this.activeOperation = this.handleZoom; + + // This function is called by the timer to perform the operation. + var self = this, // capture 'this' for use in the function + setRange = function () { + if (self.activeOperation) { + if (operation === "zoomIn") { + self.wwd.navigator.range *= (1 - self.zoomIncrement); + } else if (operation === "zoomOut") { + self.wwd.navigator.range *= (1 + self.zoomIncrement); + } + self.wwd.redraw(); + setTimeout(setRange, 50); + } + }; + setTimeout(setRange, 50); + }; + + /** + * + * @param {String} operation + */ + KeyboardControls.prototype.handlePan = function (operation) { + this.activeOperation = this.handlePan; + + // This function is called by the timer to perform the operation. + var self = this, // capture 'this' for use in the function + setLookAtLocation = function () { + if (self.activeOperation) { + var heading = self.wwd.navigator.heading, + distance = self.panIncrement * self.wwd.navigator.range; + + switch (operation) { + case 'panUp' : + break; + case 'panDown' : + heading -= 180; + break; + case 'panLeft' : + heading -= 90; + break; + case 'panRight' : + heading += 90; + break; + } + // Update the navigator's lookAtLocation + Location.greatCircleLocation( + self.wwd.navigator.lookAtLocation, + heading, + distance, + self.wwd.navigator.lookAtLocation); + self.wwd.redraw(); + setTimeout(setLookAtLocation, 50); + } + }; + setTimeout(setLookAtLocation, 50); + }; + + return KeyboardControls; + } +); + From c406f69322a9e16181f9c05f29de570a50bdcf28 Mon Sep 17 00:00:00 2001 From: Eugene Maksymenko Date: Mon, 7 Dec 2020 21:27:32 +0200 Subject: [PATCH 2/6] Replace LookAtNavigator with Camera and LookAt objects. Migrate applications to use Camera instead of Navigator. Remove Projection.geographicToLocalTransform. Rename LookAt.lookAtPosition to LookAt.position. Move Camera and LookAt to geom package. Do not convert Camera to LookAt for compass. Use Camera heading and tilt directly. Use picked terrain position instead of forward ray globe intersection. Implement camera field of view processing. Check camera altitude to avoid falling under the surface. Migrate KeyboardControls from Navigator to Camera. Migrate tests to use Camera instead of Navigator. --- apps/Explorer/Explorer.js | 6 +- apps/NEO/NEO.js | 6 +- apps/SentinelWMTS/SentinelWMTS.js | 8 +- apps/SubSurface/SubSurface.js | 6 +- apps/USGSSlabs/USGSSlabs.js | 6 +- apps/USGSWells/USGSWells.js | 12 +- examples/Canyon.html | 34 +++ examples/Canyon.js | 169 ++++++++++++ examples/ScreenImage.js | 4 +- examples/Views.html | 71 +++++ examples/Views.js | 336 ++++++++++++++++++++++++ performance/DeepPickingPerformance.js | 6 +- src/BasicWorldWindowController.js | 182 ++++++++----- src/WorldWind.js | 6 + src/WorldWindow.js | 107 +++----- src/geom/BoundingBox.js | 2 +- src/geom/Camera.js | 311 ++++++++++++++++++++++ src/geom/LookAt.js | 149 +++++++++++ src/geom/Matrix.js | 60 ++++- src/layer/ViewControlsLayer.js | 92 ++++--- src/navigate/LookAtNavigator.js | 64 ++++- src/navigate/LookAtPositionProxy.js | 70 +++++ src/navigate/Navigator.js | 67 ++++- src/render/DrawContext.js | 38 +-- src/shapes/AbstractShape.js | 4 +- src/shapes/Compass.js | 10 +- src/shapes/Placemark.js | 4 +- src/util/GoToAnimator.js | 50 ++-- src/util/KeyboardControls.js | 52 ++-- src/util/WWMath.js | 39 +-- test/BasicWorldWindowController.test.js | 72 ++--- test/WorldWindow.test.js | 46 +--- test/geom/Camera.test.js | 177 +++++++++++++ test/geom/LookAt.test.js | 107 ++++++++ test/geom/Matrix.test.js | 23 +- test/render/DrawContext.test.js | 44 +--- test/util/TestUtils.test.js | 100 +++++++ 37 files changed, 2081 insertions(+), 459 deletions(-) create mode 100644 examples/Canyon.html create mode 100644 examples/Canyon.js create mode 100644 examples/Views.html create mode 100644 examples/Views.js create mode 100644 src/geom/Camera.js create mode 100644 src/geom/LookAt.js create mode 100644 src/navigate/LookAtPositionProxy.js create mode 100644 test/geom/Camera.test.js create mode 100644 test/geom/LookAt.test.js create mode 100644 test/util/TestUtils.test.js diff --git a/apps/Explorer/Explorer.js b/apps/Explorer/Explorer.js index e5fdda0c3..77d38f8f2 100644 --- a/apps/Explorer/Explorer.js +++ b/apps/Explorer/Explorer.js @@ -64,8 +64,10 @@ define(['../../src/WorldWind', } // Start the view pointing to a longitude within the current time zone. - this.wwd.navigator.lookAtLocation.latitude = 30; - this.wwd.navigator.lookAtLocation.longitude = -(180 / 12) * ((new Date()).getTimezoneOffset() / 60); + var lookAt = new WorldWind.LookAt(); + lookAt.position.latitude = 30; + lookAt.position.longitude = -(180 / 12) * ((new Date()).getTimezoneOffset() / 60); + this.wwd.camera.setFromLookAt(lookAt); this.goToBox = new GoToBox(this.wwd); this.layersPanel = new LayersPanel(this.wwd); diff --git a/apps/NEO/NEO.js b/apps/NEO/NEO.js index 4f84ee191..7540fc683 100644 --- a/apps/NEO/NEO.js +++ b/apps/NEO/NEO.js @@ -61,8 +61,10 @@ define(['../../src/WorldWind', } // Start the view pointing to a longitude within the current time zone. - this.wwd.navigator.lookAtLocation.latitude = 30; - this.wwd.navigator.lookAtLocation.longitude = -(180 / 12) * ((new Date()).getTimezoneOffset() / 60); + var lookAt = new WorldWind.LookAt(); + lookAt.position.latitude = 30; + lookAt.position.longitude = -(180 / 12) * ((new Date()).getTimezoneOffset() / 60); + this.wwd.camera.setFromLookAt(lookAt); this.timeSeriesPlayer = new TimeSeriesPlayer(this.wwd); this.projectionMenu = new ProjectionMenu(this.wwd); diff --git a/apps/SentinelWMTS/SentinelWMTS.js b/apps/SentinelWMTS/SentinelWMTS.js index 48a67dcc4..1bc5a9e74 100644 --- a/apps/SentinelWMTS/SentinelWMTS.js +++ b/apps/SentinelWMTS/SentinelWMTS.js @@ -80,9 +80,11 @@ define(['../../src/WorldWind', } // Start the view pointing to Paris - wwd.navigator.lookAtLocation.latitude = 48.86; - wwd.navigator.lookAtLocation.longitude = 2.37; - wwd.navigator.range = 5e4; + var lookAt = new WorldWind.LookAt(); + lookAt.position.latitude = 48.86; + lookAt.position.longitude = 2.37; + lookAt.range = 5e4; + this.wwd.camera.setFromLookAt(lookAt); // Create controllers for the user interface elements. new GoToBox(wwd); diff --git a/apps/SubSurface/SubSurface.js b/apps/SubSurface/SubSurface.js index 43ff5984c..12416a67c 100644 --- a/apps/SubSurface/SubSurface.js +++ b/apps/SubSurface/SubSurface.js @@ -64,8 +64,10 @@ define(['../../src/WorldWind', this.wwd.subsurfaceMode = true; // Start the view pointing to a longitude within the current time zone. - this.wwd.navigator.lookAtLocation.latitude = 30; - this.wwd.navigator.lookAtLocation.longitude = -(180 / 12) * ((new Date()).getTimezoneOffset() / 60); + var lookAt = new WorldWind.LookAt(); + lookAt.position.latitude = 30; + lookAt.position.longitude = -(180 / 12) * ((new Date()).getTimezoneOffset() / 60); + this.wwd.camera.setFromLookAt(lookAt); this.goToBox = new GoToBox(this.wwd); this.layersPanel = new LayersPanel(this.wwd); diff --git a/apps/USGSSlabs/USGSSlabs.js b/apps/USGSSlabs/USGSSlabs.js index 842241a22..bb010f277 100644 --- a/apps/USGSSlabs/USGSSlabs.js +++ b/apps/USGSSlabs/USGSSlabs.js @@ -75,8 +75,10 @@ define(['../../src/WorldWind', this.wwd.surfaceOpacity = 0.7; // Start the view pointing to a longitude within the current time zone. - this.wwd.navigator.lookAtLocation.latitude = 30; - this.wwd.navigator.lookAtLocation.longitude = -(180 / 12) * ((new Date()).getTimezoneOffset() / 60); + var lookAt = new WorldWind.LookAt(); + lookAt.position.latitude = 30; + lookAt.position.longitude = -(180 / 12) * ((new Date()).getTimezoneOffset() / 60); + this.wwd.camera.setFromLookAt(lookAt); // Establish the shapes and the controllers to handle picking. this.setupPicking(); diff --git a/apps/USGSWells/USGSWells.js b/apps/USGSWells/USGSWells.js index 076c0dc4a..ab5de679e 100644 --- a/apps/USGSWells/USGSWells.js +++ b/apps/USGSWells/USGSWells.js @@ -85,11 +85,13 @@ define(['../../src/WorldWind', this.wwd.surfaceOpacity = 0.5; // Start the view pointing to a location near the well data. - this.wwd.navigator.lookAtLocation.latitude = 33.0977; - this.wwd.navigator.lookAtLocation.longitude = -117.0119; - this.wwd.navigator.range = 1400; - this.wwd.navigator.heading = 90; - this.wwd.navigator.tilt = 60; + var lookAt = new WorldWind.LookAt(); + lookAt.position.latitude = 33.0977; + lookAt.position.longitude = -117.0119; + lookAt.range = 1400; + lookAt.heading = 90; + lookAt.tilt = 60; + this.wwd.camera.setFromLookAt(lookAt); // Establish the shapes and the controllers to handle picking. this.setupPicking(); diff --git a/examples/Canyon.html b/examples/Canyon.html new file mode 100644 index 000000000..2abc799da --- /dev/null +++ b/examples/Canyon.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + +
+
+

WorldWind Grand Canyon Animation

+
+
+
+
+ + +
+
+
+ + Your browser does not support HTML5 Canvas. + +
+
+
+ + \ No newline at end of file diff --git a/examples/Canyon.js b/examples/Canyon.js new file mode 100644 index 000000000..9888f9f7b --- /dev/null +++ b/examples/Canyon.js @@ -0,0 +1,169 @@ +/* + * Copyright 2015-2017 WorldWind Contributors + * + * 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. + */ +requirejs(['./WorldWindShim'], + function (WorldWind) { + "use strict"; + + WorldWind.Logger.setLoggingLevel(WorldWind.Logger.LEVEL_WARNING); + + var wwd = new WorldWind.WorldWindow("canvasOne"); + var camera = wwd.camera; + var lookAt = new WorldWind.LookAt(); + camera.getAsLookAt(lookAt); + var layers = [ + {layer: new WorldWind.BMNGLayer(), enabled: true}, + {layer: new WorldWind.BingAerialWithLabelsLayer(null), enabled: true}, + {layer: new WorldWind.CompassLayer(), enabled: true}, + {layer: new WorldWind.CoordinatesDisplayLayer(wwd), enabled: true} + ]; + + for (var l = 0; l < layers.length; l++) { + layers[l].layer.enabled = layers[l].enabled; + wwd.addLayer(layers[l].layer); + } + + var atmosphereLayer = new WorldWind.AtmosphereLayer(); + wwd.addLayer(atmosphereLayer); + + var canyonCheckBox = document.getElementById('canyon'); + var canyonInterval = 0; + canyonCheckBox.addEventListener('change', onCanyonCheckBoxClick, false); + + var canyonTour = []; + canyonTour.push({ + position: new WorldWind.Position(36.17, -112.04, 2000), + heading: -150, + tilt: 70, + finished: false + }); + canyonTour.push({ + position: new WorldWind.Position(36.10, -112.10, 2000), + heading: 90, + tilt: 70, + finished: false + }); + canyonTour.push({ + position: new WorldWind.Position(36.10, -112.08, 2000), + heading: 120, + tilt: 70, + finished: false + }); + canyonTour.push({ + position: new WorldWind.Position(36.05, -111.98, 2000), + heading: 120, + tilt: 70, + finished: false + }); + + var canyonLatInc; + var canyonLonInc; + var headingInc; + var tiltInc; + var segmentStart = true; + var fromIndex = 0; + var toIndex = 1; + var fromNode = canyonTour[fromIndex]; + var toNode = canyonTour[toIndex]; + var traversalDir = 1, segHeading, segCompassHeading, headingSteps; + + function goToCanyonStartComplete() { + runCanyonSimulation(); + } + + function onCanyonCheckBoxClick() { + if (this.checked) { + wwd.goTo(fromNode.position, goToCanyonStartComplete); + } + else { + clearInterval(canyonInterval); + wwd.redraw(); + } + } + + function runCanyonSimulation() { + canyonInterval = setInterval(function () { + if (toNode.finished) { + fromIndex = toIndex; + toIndex += traversalDir; + var reset = false; + if (toIndex === canyonTour.length) { + traversalDir = -1; + reset = true; + } + else if (toIndex < 0) { + traversalDir = 1; + reset = true; + } + if (reset) { + toIndex = fromIndex + traversalDir; + for (var i = 0; i < canyonTour.length; i++) { + canyonTour[i].finished = false; + } + } + fromNode = canyonTour[fromIndex]; + toNode = canyonTour[toIndex]; + segmentStart = true; + } + + var camCompassHeading; + if (segmentStart) { + segmentStart = false; + var radiansPerFrame = 0.001 / 480 + var numFrames = Math.ceil(WorldWind.Location.greatCircleDistance(fromNode.position, toNode.position) / radiansPerFrame); + canyonLatInc = (toNode.position.latitude - fromNode.position.latitude) / numFrames; + canyonLonInc = (toNode.position.longitude - fromNode.position.longitude) / numFrames; + segHeading = traversalDir < 0 ? WorldWind.Angle.normalizedDegrees(toNode.heading + 180) : fromNode.heading; + segCompassHeading = segHeading < 0 ? segHeading + 360 : segHeading; + camCompassHeading = camera.heading < 0 ? camera.heading + 360 : camera.heading; + var headingDiff = segCompassHeading - camCompassHeading; + if (Math.abs(headingDiff) >= 180) { + headingDiff = headingDiff < 0 ? headingDiff + 360 : headingDiff - 360; + } + var angleInc = 0.25; + headingSteps = Math.floor(Math.abs(headingDiff) / angleInc); + headingInc = Math.sign(headingDiff) * angleInc; + tiltInc = Math.sign(fromNode.tilt - camera.tilt) * angleInc; + camera.position.altitude = fromNode.position.altitude; + } + if (headingSteps > 0 || tiltInc !== 0) { + camCompassHeading = camera.heading < 0 ? camera.heading + 360 : camera.heading; + camera.heading = WorldWind.Angle.normalizedDegrees(camCompassHeading + headingInc); + camera.tilt += tiltInc; + headingSteps--; + if (headingSteps <= 0) { + headingInc = 0; + camera.heading = segHeading; + } + if ((tiltInc > 0 && camera.tilt >= fromNode.tilt) || + (tiltInc < 0 && camera.tilt <= fromNode.tilt)) { + tiltInc = 0; + camera.tilt = fromNode.tilt; + } + } else { + camera.position.latitude += canyonLatInc; + camera.position.longitude += canyonLonInc; + if ((canyonLatInc > 0 && camera.position.latitude > toNode.position.latitude) || + (canyonLatInc < 0 && camera.position.latitude < toNode.position.latitude) || + (canyonLonInc > 0 && camera.position.longitude > toNode.position.longitude) || + (canyonLonInc < 0 && camera.position.longitude < toNode.position.longitude)) { + toNode.finished = true; + } + } + wwd.redraw(); + }, 25); + } + + }); \ No newline at end of file diff --git a/examples/ScreenImage.js b/examples/ScreenImage.js index d1cba2445..f8885ae61 100644 --- a/examples/ScreenImage.js +++ b/examples/ScreenImage.js @@ -110,9 +110,9 @@ requirejs(['./WorldWindShim', if (pickList.objects.length > 0) { for (var p = 0; p < pickList.objects.length; p++) { - // If the compass is picked, reset the navigator heading to 0 to re-orient the globe. + // If the compass is picked, reset the camera heading to 0 to re-orient the globe. if (pickList.objects[p].userObject instanceof WorldWind.Compass) { - wwd.navigator.heading = 0; + wwd.camera.heading = 0; wwd.redraw(); } else if (pickList.objects[p].userObject instanceof WorldWind.ScreenImage) { diff --git a/examples/Views.html b/examples/Views.html new file mode 100644 index 000000000..34368171b --- /dev/null +++ b/examples/Views.html @@ -0,0 +1,71 @@ + + + + + + + + + + + + +
+ +
+
+

Projection

+ +

Layers

+
+
+
+ + + + +
+
View Type:
+ +
Latitude
+
+
Longitude
+
+
Altitude
+
+
Heading
+
+
Tilt
+
+
Roll
+
+
Range
+
+
+

Destination

+ +
+
+ + Your browser does not support HTML5 Canvas. + +
+
+
+ + \ No newline at end of file diff --git a/examples/Views.js b/examples/Views.js new file mode 100644 index 000000000..d4326c06d --- /dev/null +++ b/examples/Views.js @@ -0,0 +1,336 @@ +/* + * Copyright 2015-2017 WorldWind Contributors + * + * 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. + */ +requirejs(['./WorldWindShim', + './LayerManager'], + function (WorldWind, + LayerManager) { + "use strict"; + + WorldWind.Logger.setLoggingLevel(WorldWind.Logger.LEVEL_WARNING); + + var wwd = new WorldWind.WorldWindow("canvasOne"); + var camera = wwd.camera; + var lookAt = new WorldWind.LookAt(); + camera.getAsLookAt(lookAt); + var layers = [ + {layer: new WorldWind.BMNGLayer(), enabled: true}, + {layer: new WorldWind.BingAerialWithLabelsLayer(null), enabled: true}, + {layer: new WorldWind.CompassLayer(), enabled: true}, + {layer: new WorldWind.CoordinatesDisplayLayer(wwd), enabled: true}, + {layer: new WorldWind.ViewControlsLayer(wwd), enabled: true} + ]; + + for (var l = 0; l < layers.length; l++) { + layers[l].layer.enabled = layers[l].enabled; + wwd.addLayer(layers[l].layer); + } + + var atmosphereLayer = new WorldWind.AtmosphereLayer(); + wwd.addLayer(atmosphereLayer); + + // Create a layer manager for controlling layer visibility. + var layerManager = new LayerManager(wwd); + var selectedViewType = "Camera"; + $("#select-view").change(function () { + selectedViewType = ""; + $("select option:selected").each(function () { + selectedViewType += $(this).text(); + }); + updateView(); + }); + + var updateView; + + var addSlider = function (valueControl, sliderControl, min, max, step, defaultValue) { + sliderControl.slider({ + value: min, min: min, max: max, step: step, animate: true, + slide: function (event, ui) { + valueControl.html(ui.value); + updateView(); + } + }); + sliderControl.slider('value', defaultValue); + valueControl.html(defaultValue); + }; + + var latitudeValue = $("#latitude"); + var latitudeSlider = $("#latitudeSlider"); + addSlider(latitudeValue, latitudeSlider, -90, 90, 1, camera.position.latitude); + + var longitudeValue = $("#longitude"); + var longitudeSlider = $("#longitudeSlider"); + addSlider(longitudeValue, longitudeSlider, -180, 180, 1, camera.position.longitude); + + var altitudeValue = $("#altitude"); + var altitudeSlider = $("#altitudeSlider"); + addSlider(altitudeValue, altitudeSlider, 1, 10e6, 1, camera.position.altitude); + + var rangeValue = $("#range"); + var rangeSlider = $("#rangeSlider"); + addSlider(rangeValue, rangeSlider, 1, 10e6, 1, lookAt.range); + rangeSlider.slider("disable"); + + var headingValue = $("#heading"); + var headingSlider = $("#headingSlider"); + addSlider(headingValue, headingSlider, -180, 180, 1, camera.heading); + + var tiltValue = $("#tilt"); + var tiltSlider = $("#tiltSlider"); + addSlider(tiltValue, tiltSlider, -90, 90, 1, camera.tilt); + + var rollValue = $("#roll"); + var rollSlider = $("#rollSlider"); + addSlider(rollValue, rollSlider, -90, 90, 1, camera.roll); + + var updateControls = function (pos, selectedView) { + var precision = 10000.0; + latitudeValue.html(Math.round(pos.latitude * precision) / precision); + longitudeValue.html(Math.round(pos.longitude * precision) / precision); + altitudeValue.html(Math.round(pos.altitude * precision) / precision); + headingValue.html(Math.round(selectedView.heading * precision) / precision); + tiltValue.html(Math.round(selectedView.tilt * precision) / precision); + rollValue.html(Math.round(selectedView.roll * precision) / precision); + + latitudeSlider.slider('value', pos.latitude); + longitudeSlider.slider('value', pos.longitude); + altitudeSlider.slider('value', pos.altitude); + headingSlider.slider('value', selectedView.heading); + tiltSlider.slider('value', selectedView.tilt); + rollSlider.slider('value', selectedView.roll); + if (selectedView === lookAt) { + rangeValue.html(Math.round(lookAt.range * 100.0) / 100.0); + rangeSlider.slider('value', lookAt.range); + } + }; + + var currentViewType = selectedViewType; + updateView = function () { + var pos, view; + if (selectedViewType !== currentViewType) { + currentViewType = selectedViewType; + if (currentViewType === "Camera") { + pos = camera.position; + view = camera; + rangeSlider.slider("disable"); + altitudeSlider.slider("enable"); + } else { + camera.getAsLookAt(lookAt); + pos = lookAt.position; + view = lookAt; + rangeSlider.slider("enable"); + altitudeSlider.slider("disable"); + } + } else { + if (currentViewType === "Camera") { + pos = camera.position; + view = camera; + } else { + camera.getAsLookAt(lookAt); + pos = lookAt.position; + view = lookAt; + } + pos.latitude = latitudeSlider.slider("value"); + pos.longitude = longitudeSlider.slider("value"); + pos.altitude = altitudeSlider.slider("value"); + view.heading = headingSlider.slider("value"); + view.tilt = tiltSlider.slider("value"); + view.roll = rollSlider.slider("value"); + if (selectedViewType === "LookAt") { + lookAt.range = rangeSlider.slider("value"); + camera.setFromLookAt(lookAt); + } + } + updateControls(pos, view); + wwd.redraw(); + }; + + window.setInterval(function () { + var pos, view; + camera.getAsLookAt(lookAt); + if (currentViewType === "Camera") { + pos = camera.position; + view = camera; + } else { + pos = lookAt.position; + view = lookAt; + } + updateControls(pos, view); + }, 100); + + var canyonCheckBox = document.getElementById('canyon'); + var canyonInterval = 0; + canyonCheckBox.addEventListener('change', onCanyonCheckBoxClick, false); + + var canyonTour = []; + canyonTour.push({ + position: new WorldWind.Position(36.17, -112.04, 2000), + heading: -150, + tilt: 70, + finished: false + }); + canyonTour.push({ + position: new WorldWind.Position(36.10, -112.10, 2000), + heading: 90, + tilt: 70, + finished: false + }); + canyonTour.push({ + position: new WorldWind.Position(36.10, -112.08, 2000), + heading: 120, + tilt: 70, + finished: false + }); + canyonTour.push({ + position: new WorldWind.Position(36.05, -111.98, 2000), + heading: 120, + tilt: 70, + finished: false + }); + + var canyonLatInc; + var canyonLonInc; + var headingInc; + var tiltInc; + var segmentStart = true; + var fromIndex = 0; + var toIndex = 1; + var fromNode = canyonTour[fromIndex]; + var toNode = canyonTour[toIndex]; + var traversalDir = 1, segHeading, segCompassHeading, headingSteps; + + function goToCanyonStartComplete() { + runCanyonSimulation(); + } + + function onCanyonCheckBoxClick() { + if (this.checked) { + wwd.goTo(fromNode.position, goToCanyonStartComplete); + // camera.position.copy(fromNode.position); + // goToCanyonStartComplete(); + } + else { + clearInterval(canyonInterval); + wwd.redraw(); + } + } + + function runCanyonSimulation() { + canyonInterval = setInterval(function () { + if (toNode.finished) { + fromIndex = toIndex; + toIndex += traversalDir; + var reset = false; + if (toIndex === canyonTour.length) { + traversalDir = -1; + reset = true; + } + else if (toIndex < 0) { + traversalDir = 1; + reset = true; + } + if (reset) { + toIndex = fromIndex + traversalDir; + for (var i = 0; i < canyonTour.length; i++) { + canyonTour[i].finished = false; + } + } + fromNode = canyonTour[fromIndex]; + toNode = canyonTour[toIndex]; + segmentStart = true; + } + + var camCompassHeading; + if (segmentStart) { + segmentStart = false; + var radiansPerFrame = 0.001 / 480 + var numFrames = Math.ceil(WorldWind.Location.greatCircleDistance(fromNode.position, toNode.position) / radiansPerFrame); + canyonLatInc = (toNode.position.latitude - fromNode.position.latitude) / numFrames; + canyonLonInc = (toNode.position.longitude - fromNode.position.longitude) / numFrames; + segHeading = traversalDir < 0 ? WorldWind.Angle.normalizedDegrees(toNode.heading + 180) : fromNode.heading; + segCompassHeading = segHeading < 0 ? segHeading + 360 : segHeading; + camCompassHeading = camera.heading < 0 ? camera.heading + 360 : camera.heading; + var headingDiff = segCompassHeading - camCompassHeading; + if (Math.abs(headingDiff) >= 180) { + headingDiff = headingDiff < 0 ? headingDiff + 360 : headingDiff - 360; + } + var angleInc = 0.25; + headingSteps = Math.floor(Math.abs(headingDiff) / angleInc); + headingInc = Math.sign(headingDiff) * angleInc; + tiltInc = Math.sign(fromNode.tilt - camera.tilt) * angleInc; + camera.position.altitude = fromNode.position.altitude; + } + if (headingSteps > 0 || tiltInc !== 0) { + camCompassHeading = camera.heading < 0 ? camera.heading + 360 : camera.heading; + camera.heading = WorldWind.Angle.normalizedDegrees(camCompassHeading + headingInc); + camera.tilt += tiltInc; + headingSteps--; + if (headingSteps <= 0) { + headingInc = 0; + camera.heading = segHeading; + } + if ((tiltInc > 0 && camera.tilt >= fromNode.tilt) || + (tiltInc < 0 && camera.tilt <= fromNode.tilt)) { + tiltInc = 0; + camera.tilt = fromNode.tilt; + } + } else { + camera.position.latitude += canyonLatInc; + camera.position.longitude += canyonLonInc; + if ((canyonLatInc > 0 && camera.position.latitude > toNode.position.latitude) || + (canyonLatInc < 0 && camera.position.latitude < toNode.position.latitude) || + (canyonLonInc > 0 && camera.position.longitude > toNode.position.longitude) || + (canyonLonInc < 0 && camera.position.longitude < toNode.position.longitude)) { + toNode.finished = true; + } + } + wwd.redraw(); + }, 25); + } + + var orbitCheckBox = document.getElementById('orbit'); + var orbitInterval = 0; + orbitCheckBox.addEventListener('change', onOrbitCheckBoxClick, false); + + function goToOrbitStartComplete() { + runOrbitSimulation(); + } + + var orbitStartPos = new WorldWind.Position(30, -70, 5e6); + + function onOrbitCheckBoxClick() { + if (this.checked) { + wwd.goTo(orbitStartPos, goToOrbitStartComplete); + } + else { + clearInterval(orbitInterval); + wwd.redraw(); + } + } + + function runOrbitSimulation() { + orbitInterval = setInterval(function () { + camera.position.latitude = orbitStartPos.latitude; + camera.position.longitude -= 0.1; + camera.position.altitude = orbitStartPos.altitude; + + if (camera.position.longitude < -180) { + camera.position.longitude = 180; + } + + wwd.redraw(); + }, 25); + } + }); \ No newline at end of file diff --git a/performance/DeepPickingPerformance.js b/performance/DeepPickingPerformance.js index ae9057a12..9f2b5335a 100644 --- a/performance/DeepPickingPerformance.js +++ b/performance/DeepPickingPerformance.js @@ -66,8 +66,10 @@ requirejs(['../src/WorldWind', wwd.deepPicking = true; // Start out zoomed to the AOI - wwd.navigator.lookAtLocation = new WorldWind.Location(44.2, -94.12); - wwd.navigator.range = 625000; + var lookAt = new WorldWind.LookAt(); + lookAt.position = new WorldWind.Position(44.2, -94.12, 0); + lookAt.range = 625000; + wwd.camera.setFromLookAt(lookAt); // Satellite image footprints var footprints = []; diff --git a/src/BasicWorldWindowController.js b/src/BasicWorldWindowController.js index aba0b03da..1bb9c5222 100644 --- a/src/BasicWorldWindowController.js +++ b/src/BasicWorldWindowController.js @@ -31,10 +31,12 @@ define([ './geom/Angle', './error/ArgumentError', + './geom/Camera', './gesture/ClickRecognizer', './gesture/DragRecognizer', './gesture/GestureRecognizer', './util/Logger', + './geom/LookAt', './geom/Matrix', './gesture/PanRecognizer', './gesture/PinchRecognizer', @@ -49,10 +51,12 @@ define([ ], function (Angle, ArgumentError, + Camera, ClickRecognizer, DragRecognizer, GestureRecognizer, Logger, + LookAt, Matrix, PanRecognizer, PinchRecognizer, @@ -126,10 +130,21 @@ define([ // 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; + + /** + * Internal use only. + * A copy of the viewing parameters at the start of a gesture as a look at view. + * @ignore + */ + this.beginLookAt = new LookAt(); + + /** + * Internal use only. + * The current state of the viewing parameters during a gesture as a look at view. + * @ignore + */ + this.lookAt = new LookAt(); }; BasicWorldWindowController.prototype = Object.create(WorldWindowController.prototype); @@ -211,31 +226,33 @@ define([ tx = recognizer.translationX, ty = recognizer.translationY; - var navigator = this.wwd.navigator; if (state === WorldWind.BEGAN) { + this.gestureDidBegin(); 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 + // Convert the translation from screen coordinates to arc degrees. Use the view'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, + var lookAt = this.lookAt, + canvas = this.wwd.canvas, globe = this.wwd.globe, globeRadius = WWMath.max(globe.equatorialRadius, globe.polarRadius), - distance = WWMath.max(1, navigator.range), + distance = WWMath.max(1, lookAt.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); + // Apply the change in latitude and longitude to the view, relative to the current heading. + var sinHeading = Math.sin(lookAt.heading * Angle.DEGREES_TO_RADIANS), + cosHeading = Math.cos(lookAt.heading * Angle.DEGREES_TO_RADIANS); - navigator.lookAtLocation.latitude += forwardDegrees * cosHeading - sideDegrees * sinHeading; - navigator.lookAtLocation.longitude += forwardDegrees * sinHeading + sideDegrees * cosHeading; + lookAt.position.latitude += forwardDegrees * cosHeading - sideDegrees * sinHeading; + lookAt.position.longitude += forwardDegrees * sinHeading + sideDegrees * cosHeading; this.lastPoint.set(tx, ty); - this.applyLimits(); + this.applyLookAtLimits(lookAt); + this.wwd.camera.setFromLookAt(lookAt); this.wwd.redraw(); } }; @@ -248,8 +265,8 @@ define([ tx = recognizer.translationX, ty = recognizer.translationY; - var navigator = this.wwd.navigator; if (state === WorldWind.BEGAN) { + this.gestureDidBegin(); this.beginPoint.set(x, y); this.lastPoint.set(x, y); } else if (state === WorldWind.CHANGED) { @@ -260,7 +277,8 @@ define([ this.lastPoint.set(x2, y2); - var globe = this.wwd.globe, + var lookAt = this.lookAt, + 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), @@ -275,27 +293,28 @@ define([ return; } - // Transform the original navigator state's modelview matrix to account for the gesture's change. + // Transform the original view's modelview matrix to account for the gesture's change. var modelview = Matrix.fromIdentity(); - this.wwd.computeViewingTransform(null, modelview); + lookAt.computeViewingTransform(globe, 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. + // Compute the globe point at the screen center from the perspective of the transformed view. 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(); + // Convert the transformed modelview matrix to a set of view properties, then apply those + // properties to this view. + var params = modelview.extractViewingParameters(origin, lookAt.roll, globe, {}); + lookAt.position.copy(params.origin); + lookAt.range = params.range; + lookAt.heading = params.heading; + lookAt.tilt = params.tilt; + lookAt.roll = params.roll; + this.applyLookAtLimits(lookAt); + this.wwd.camera.setFromLookAt(lookAt); this.wwd.redraw(); } }; @@ -306,38 +325,39 @@ define([ tx = recognizer.translationX, ty = recognizer.translationY; - var navigator = this.wwd.navigator; if (state === WorldWind.BEGAN) { - this.beginHeading = navigator.heading; - this.beginTilt = navigator.tilt; + this.gestureDidBegin(); } 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, + var lookAt = this.lookAt, + 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(); + // Apply the change in heading and tilt to this view's corresponding properties. + lookAt.heading = this.beginLookAt.heading + headingDegrees; + lookAt.tilt = this.beginLookAt.tilt + tiltDegrees; + this.applyLookAtLimits(lookAt); + this.wwd.camera.setFromLookAt(lookAt); 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; + this.gestureDidBegin(); } 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 + // Apply the change in pinch scale to this view's range, relative to the range when the gesture // began. - navigator.range = this.beginRange / scale; - this.applyLimits(); + var lookAt = this.lookAt; + lookAt.range = this.beginLookAt.range / scale; + this.applyLookAtLimits(lookAt); + this.wwd.camera.setFromLookAt(lookAt); this.wwd.redraw(); } } @@ -345,45 +365,48 @@ define([ // 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.gestureDidBegin(); 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 + // Apply the change in gesture rotation to this view'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; + var lookAt = this.lookAt; + lookAt.heading -= rotation - this.lastRotation; this.lastRotation = rotation; - this.applyLimits(); + this.applyLookAtLimits(lookAt); + this.wwd.camera.setFromLookAt(lookAt); 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; + this.gestureDidBegin(); } 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(); + // Apply the change in heading and tilt to this view's corresponding properties. + var lookAt = this.lookAt; + lookAt.tilt = this.beginTilt + tiltDegrees; + this.applyLookAtLimits(lookAt); + this.wwd.camera.setFromLookAt(lookAt); this.wwd.redraw(); } }; // Intentionally not documented. BasicWorldWindowController.prototype.handleWheelEvent = function (event) { - var navigator = this.wwd.navigator; + var lookAt = this.wwd.camera.getAsLookAt(this.lookAt); // Normalize the wheel delta based on the wheel delta mode. This produces a roughly consistent delta across // browsers and input devices. var normalizedDelta; @@ -396,49 +419,72 @@ define([ } // 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 + // view'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(); + // Apply the scale to this view's properties. + lookAt.range *= scale; + this.applyLookAtLimits(lookAt); + this.wwd.camera.setFromLookAt(lookAt); this.wwd.redraw(); }; - // Documented in super-class. - BasicWorldWindowController.prototype.applyLimits = function () { - var navigator = this.wwd.navigator; - + /** + * Internal use only. + * Limits the properties of a look at view to prevent unwanted navigation behaviour. + * @ignore + */ + BasicWorldWindowController.prototype.applyLookAtLimits = function (lookAt) { // 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); + lookAt.position.latitude = WWMath.clamp(lookAt.position.latitude, -90, 90); + lookAt.position.longitude = Angle.normalizedDegreesLongitude(lookAt.position.longitude); - // Clamp range to values greater than 1 in order to prevent degenerating to a first-person navigator when + // Clamp range to values greater than 1 in order to prevent degenerating to a first-person lookAt when // range is zero. - navigator.range = WWMath.clamp(navigator.range, 1, Number.MAX_VALUE); + lookAt.range = WWMath.clamp(lookAt.range, 1, Number.MAX_VALUE); // Normalize heading to between -180 and +180. - navigator.heading = Angle.normalizedDegrees(navigator.heading); + lookAt.heading = Angle.normalizedDegrees(lookAt.heading); // Clamp tilt to between 0 and +90 to prevent the viewer from going upside down. - navigator.tilt = WWMath.clamp(navigator.tilt, 0, 90); + lookAt.tilt = WWMath.clamp(lookAt.tilt, 0, 90); // Normalize heading to between -180 and +180. - navigator.roll = Angle.normalizedDegrees(navigator.roll); + lookAt.roll = Angle.normalizedDegrees(lookAt.roll); // Apply 2D limits when the globe is 2D. - if (this.wwd.globe.is2D() && navigator.enable2DLimits) { + if (this.wwd.globe.is2D()) { // 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); + lookAt.range = WWMath.clamp(lookAt.range, 1, maxRange); // Force tilt to 0 when in 2D mode to keep the viewer looking straight down. - navigator.tilt = 0; + lookAt.tilt = 0; } }; + /** + * Documented in super-class. + * @ignore + */ + BasicWorldWindowController.prototype.applyLimits = function () { + var lookAt = this.wwd.camera.getAsLookAt(this.lookAt); + this.applyLookAtLimits(lookAt); + this.wwd.camera.setFromLookAt(lookAt); + }; + + /** + * Internal use only. + * Sets common variables at the beginning of gesture. + * @ignore + */ + BasicWorldWindowController.prototype.gestureDidBegin = function () { + this.wwd.camera.getAsLookAt(this.beginLookAt); + this.lookAt.copy(this.beginLookAt); + }; + return BasicWorldWindowController; } ); diff --git a/src/WorldWind.js b/src/WorldWind.js index 46c0059a4..e492329dc 100644 --- a/src/WorldWind.js +++ b/src/WorldWind.js @@ -51,6 +51,7 @@ define([ // PLEASE KEEP ALL THIS IN ALPHABETICAL ORDER BY MODULE NAME (not direc './layer/BMNGOneImageLayer', './layer/BMNGRestLayer', './geom/BoundingBox', + './geom/Camera', './gesture/ClickRecognizer', './formats/collada/ColladaLoader', './util/Color', @@ -185,6 +186,7 @@ define([ // PLEASE KEEP ALL THIS IN ALPHABETICAL ORDER BY MODULE NAME (not direc './geom/Line', './geom/Location', './util/Logger', + './geom/LookAt', './navigate/LookAtNavigator', './geom/Matrix', './geom/MeasuredLocation', @@ -329,6 +331,7 @@ define([ // PLEASE KEEP ALL THIS IN ALPHABETICAL ORDER BY MODULE NAME (not direc BMNGOneImageLayer, BMNGRestLayer, BoundingBox, + Camera, ClickRecognizer, ColladaLoader, Color, @@ -463,6 +466,7 @@ define([ // PLEASE KEEP ALL THIS IN ALPHABETICAL ORDER BY MODULE NAME (not direc Line, Location, Logger, + LookAt, LookAtNavigator, Matrix, MeasuredLocation, @@ -844,6 +848,7 @@ define([ // PLEASE KEEP ALL THIS IN ALPHABETICAL ORDER BY MODULE NAME (not direc WorldWind['BMNGOneImageLayer'] = BMNGOneImageLayer; WorldWind['BMNGRestLayer'] = BMNGRestLayer; WorldWind['BoundingBox'] = BoundingBox; + WorldWind['Camera'] = Camera; WorldWind['ClickRecognizer'] = ClickRecognizer; WorldWind['ColladaLoader'] = ColladaLoader; WorldWind['Color'] = Color; @@ -978,6 +983,7 @@ define([ // PLEASE KEEP ALL THIS IN ALPHABETICAL ORDER BY MODULE NAME (not direc WorldWind['Line'] = Line; WorldWind['Location'] = Location; WorldWind['Logger'] = Logger; + WorldWind['LookAt'] = LookAt; WorldWind['LookAtNavigator'] = LookAtNavigator; WorldWind['Matrix'] = Matrix; WorldWind['MeasuredLocation'] = MeasuredLocation; diff --git a/src/WorldWindow.js b/src/WorldWindow.js index c77a9c50c..c88b41266 100644 --- a/src/WorldWindow.js +++ b/src/WorldWindow.js @@ -31,6 +31,7 @@ define([ './error/ArgumentError', './BasicWorldWindowController', + './geom/Camera', './render/DrawContext', './globe/EarthElevationModel', './util/FrameStatistics', @@ -57,6 +58,7 @@ define([ ], function (ArgumentError, BasicWorldWindowController, + Camera, DrawContext, EarthElevationModel, FrameStatistics, @@ -125,7 +127,7 @@ define([ // Internal. Intentionally not documented. this.drawContext = new DrawContext(gl); - // Internal. Intentionally not documented. Must be initialized before the navigator is created. + // Internal. Intentionally not documented. this.eventListeners = {}; // Internal. Intentionally not documented. Initially true in order to redraw at least once. @@ -181,11 +183,20 @@ define([ this.layers = []; /** - * The navigator used to manipulate the globe. + * The deprecated navigator that can be used to manipulate the globe. See the {@link Camera} and {@link LookAt} + * classes for replacement functionality. + * @deprecated * @type {LookAtNavigator} * @default [LookAtNavigator]{@link LookAtNavigator} */ - this.navigator = new LookAtNavigator(); + this.navigator = new LookAtNavigator(this); + + /** + * The camera used to view the globe. + * @type {Camera} + * @default [Camera]{@link Camera} + */ + this.camera = new Camera(this); /** * The controller used to manipulate the globe. @@ -394,13 +405,7 @@ define([ * 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. + * When an event occurs, this calls the registered event listeners in order of reverse registration. * * @param type The event type to listen for. * @param listener The function to call when the event occurs. @@ -699,24 +704,18 @@ define([ 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); + this.camera.computeViewingTransform(modelview); 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)), + var globeRadius = WWMath.max(this.globe.equatorialRadius, this.globe.polarRadius), + eyePos = this.camera.position, + fieldOfView = this.camera.fieldOfView, 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 adjust the clip plane distances based on the camera's orientation - shorter distances when the // TODO horizon is not in view // TODO parameterize the object altitude for horizon distance var farDistance = eyeHorizon + atmosphereHorizon; @@ -730,9 +729,9 @@ define([ 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); + var distanceToSurface = eyePos.altitude - this.globe.elevationAtLocation(eyePos.latitude, eyePos.longitude) * this.verticalExaggeration; if (distanceToSurface > 0) { - var maxNearDistance = WWMath.perspectiveNearDistance(viewport.width, viewport.height, distanceToSurface); + var maxNearDistance = WWMath.perspectiveNearDistance(fieldOfView, distanceToSurface); if (nearDistance > maxNearDistance) { nearDistance = maxNearDistance; } @@ -742,49 +741,13 @@ define([ nearDistance = 1; } - // Compute the current projection matrix based on this navigator's perspective properties and the current + // Compute the current projection matrix based on this camera's perspective properties and the current // WebGL viewport. - projection.setToPerspectiveProjection(viewport.width, viewport.height, nearDistance, farDistance); + projection.setToIdentity(); + projection.setToPerspectiveProjection(viewport.width, viewport.height, fieldOfView, 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. *

@@ -799,9 +762,9 @@ define([ * 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; + var tanfovy_2 = Math.tan(this.camera.fieldOfView * 0.5 / 180.0 * Math.PI); + var frustumHeight = 2 * distance * tanfovy_2; + return frustumHeight / this.viewport.height; }; // Internal. Intentionally not documented. @@ -815,12 +778,7 @@ define([ 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. + // Compute the inverse of the modelview, projection, and modelview-projection matrices. var modelviewInv = Matrix.fromIdentity(); modelviewInv.invertOrthonormalMatrix(dc.modelview); @@ -846,6 +804,7 @@ define([ dc.reset(); dc.globe = this.globe; dc.navigator = this.navigator; + dc.camera = this.camera; dc.layers = this.layers.slice(); dc.layers.push(dc.screenCreditController); this.computeDrawContext(); @@ -1452,10 +1411,10 @@ define([ }; /** - * 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 + * Moves this WorldWindow's camera to a specified look at location or position. + * @param {Location | Position} position The location or position to move the look at 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. + * at the specified altitude. Otherwise the end point is at the current altitude of the camera. * * 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. diff --git a/src/geom/BoundingBox.js b/src/geom/BoundingBox.js index 4953f707e..345ed764f 100644 --- a/src/geom/BoundingBox.js +++ b/src/geom/BoundingBox.js @@ -571,7 +571,7 @@ define([ try { // Setup to transform unit cube coordinates to this bounding box's local coordinates, as viewed by the - // current navigator state. + // current camera state. matrix.copy(dc.modelviewProjection); matrix.multiply( this.r[0], this.s[0], this.t[0], this.center[0], diff --git a/src/geom/Camera.js b/src/geom/Camera.js new file mode 100644 index 000000000..8684bf3b7 --- /dev/null +++ b/src/geom/Camera.js @@ -0,0 +1,311 @@ +/* + * Copyright 2015-2017 WorldWind Contributors + * + * 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 Camera + */ +define([ + '../error/ArgumentError', + '../geom/Line', + '../util/Logger', + '../geom/LookAt', + '../geom/Matrix', + '../geom/Position', + '../geom/Vec3', + '../util/WWMath' + ], + function (ArgumentError, + Line, + Logger, + LookAt, + Matrix, + Position, + Vec3, + WWMath) { + "use strict"; + + var Camera = function (worldWindow) { + if (!worldWindow) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Camera", "constructor", "missingWorldWindow")); + } + + /** + * The WorldWindow associated with this camera. + * @type {WorldWindow} + * @readonly + */ + this.wwd = worldWindow; + + /** + * The geographic location of the camera. + * @type {Location} + */ + this.position = new Position(30, -110, 10e6); + + /** + * Camera heading, in degrees clockwise from north. + * @type {Number} + * @default 0 + */ + this.heading = 0; + + /** + * Camera tilt, in degrees. + * @default 0 + */ + this.tilt = 0; + + /** + * Camera roll, in degrees. + * @type {Number} + * @default 0 + */ + this.roll = 0; + + /** + * Camera vertical field of view, in degrees + * @type {Number} + * @default 45 + */ + this.fieldOfView = 45; + + /** + * Internal use only. + * A temp variable used to hold model view matrices during calculations. Using an object level temp property + * negates the need for ad-hoc allocations and reduces load on the garbage collector. + * @ignore + */ + this.scratchModelview = Matrix.fromIdentity(); + + /** + * Internal use only. + * A temp variable used to hold points during calculations. Using an object level temp property + * negates the need for ad-hoc allocations and reduces load on the garbage collector. + * @ignore + */ + this.scratchPoint = new Vec3(0, 0, 0); + + /** + * Internal use only. + * A temp variable used to hold origin matrices during calculations. Using an object level temp property + * negates the need for ad-hoc allocations and reduces load on the garbage collector. + * @ignore + */ + this.scratchOrigin = Matrix.fromIdentity(); + + /** + * Internal use only. + * A temp variable used to hold positions during calculations. Using an object level temp property + * negates the need for ad-hoc allocations and reduces load on the garbage collector. + * @ignore + */ + this.scratchPosition = new Position(0, 0, 0); + + /** + * Internal use only. + * A temp variable used to hold lines during calculations. Using an object level temp property + * negates the need for ad-hoc allocations and reduces load on the garbage collector. + * @ignore + */ + this.scratchRay = new Line(new Vec3(0, 0, 0), new Vec3(0, 0, 0)); + }; + + /** + * Internal use only. + * Computes the model view matrix for this camera. + * @ignore + */ + Camera.prototype.computeViewingTransform = function (modelview) { + if (!modelview) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Camera", "computeViewingTransform", "missingModelview")); + } + + modelview.setToIdentity(); + modelview.multiplyByFirstPersonModelview(this.position, this.heading, this.tilt, this.roll, this.wwd.globe); + + return modelview; + }; + + /** + * Indicates whether the components of this object are equal to those of a specified object. + * @param {Camera} otherView The object to test equality with. May be null or undefined, in which case this + * function returns false. + * @returns {boolean} true if all components of this object are equal to the corresponding + * components of the specified object, otherwise false. + */ + Camera.prototype.equals = function (otherView) { + if (otherView) { + return this.position.equals(otherView.position) && + this.heading === otherView.heading && + this.tilt === otherView.tilt && + this.roll === otherView.roll; + } + + return false; + }; + + /** + * Creates a new object that is a copy of this object. + * @returns {Camera} The new object. + */ + Camera.prototype.clone = function () { + var clone = new Camera(this.wwd); + clone.copy(this); + + return clone; + }; + + /** + * Copies the components of a specified object to this object. + * @param {Camera} copyObject The object to copy. + * @returns {Camera} A copy of this object equal to copyObject. + * @throws {ArgumentError} If the specified object is null or undefined. + */ + Camera.prototype.copy = function (copyObject) { + if (!copyObject) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Camera", "copy", "missingObject")); + } + + this.wwd = copyObject.wwd; + this.position.copy(copyObject.position); + this.heading = copyObject.heading; + this.tilt = copyObject.tilt; + this.roll = copyObject.roll; + + return this; + }; + + /** + * Sets the properties of this Camera such that it mimics the supplied look at view. Note that repeated conversions + * between a look at and a camera view may result in view errors due to rounding. + * @param {LookAt} lookAt The look at view to mimic. + * @returns {Camera} This camera set to mimic the supplied look at view. + * @throws {ArgumentError} If the specified look at view is null or undefined. + */ + Camera.prototype.setFromLookAt = function (lookAt) { + if (!lookAt) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Camera", "setFromLookAt", "missingLookAt")); + } + + var globe = this.wwd.globe, + ve = this.wwd.verticalExaggeration, + ray = this.scratchRay, + originPoint = this.scratchPoint, + modelview = this.scratchModelview, + origin = this.scratchOrigin; + + lookAt.computeViewingTransform(globe, modelview); + modelview.extractEyePoint(originPoint); + + globe.computePositionFromPoint(originPoint[0], originPoint[1], originPoint[2], this.position); + origin.setToIdentity(); + origin.multiplyByLocalCoordinateTransform(originPoint, globe); + modelview.multiplyMatrix(origin); + + this.heading = modelview.extractHeading(lookAt.roll); // disambiguate heading and roll + this.tilt = modelview.extractTilt(); + this.roll = lookAt.roll; // roll passes straight through + + // Check if camera altitude is not under the surface and correct tilt + var elevation = globe.elevationAtLocation(this.position.latitude, this.position.longitude) * ve + 10.0; // 10m above surface + if(elevation > this.position.altitude) { + // Set camera altitude above the surface + this.position.altitude = elevation; + // Compute new camera point + globe.computePointFromPosition(this.position.latitude, this.position.longitude, this.position.altitude, originPoint); + // Compute look at point + globe.computePointFromPosition(lookAt.position.latitude, lookAt.position.longitude, lookAt.position.altitude, ray.origin); + // Compute normal to globe in look at point + globe.surfaceNormalAtLocation(lookAt.position.latitude, lookAt.position.longitude, ray.direction); + // Calculate tilt angle between new camera point and look at point + originPoint.subtract(ray.origin).normalize(); + var dot = ray.direction.dot(originPoint); + if (dot >= -1 || dot <= 1) { + this.tilt = Math.acos(dot) / Math.PI * 180; + } + } + + return this; + }; + + /** + * Converts the properties of this Camera to those of a look at view. Note that repeated conversions + * between a look at and a camera view may result in view errors due to rounding. + * @param {LookAt} result The look at view to hold the converted properties. + * @returns {LookAt} A reference to the result parameter. + * @throws {ArgumentError} If the specified result object is null or undefined. + */ + Camera.prototype.getAsLookAt = function (result) { + if (!result) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Camera", "getAsLookAt", "missingResult")); + } + + var globe = this.wwd.globe, + viewport = this.wwd.viewport, + forwardRay = this.scratchRay, + modelview = this.scratchModelview, + originPoint = this.scratchPoint, + originPos = this.scratchPosition, + origin = this.scratchOrigin; + + this.computeViewingTransform(modelview); + + // Pick terrain located behind the viewport center point + var terrainObject = this.wwd.pick([viewport.width / 2, viewport.height / 2]).terrainObject(); + if (terrainObject) { + // Use picked terrain position including approximate rendered altitude + originPos.copy(terrainObject.position); + globe.computePointFromPosition(originPos.latitude, originPos.longitude, originPos.altitude, originPoint); + } else { + // Center is outside the globe - use point on horizon + modelview.extractEyePoint(forwardRay.origin); + modelview.extractForwardVector(forwardRay.direction); + + var globeRadius = WWMath.max(globe.equatorialRadius, globe.polarRadius); + var horizon = WWMath.horizonDistanceForGlobeRadius(globeRadius, this.position.altitude); + forwardRay.pointAt(horizon, originPoint); + + globe.computePositionFromPoint(originPoint[0], originPoint[1], originPoint[2], originPos); + } + + origin.setToIdentity(); + origin.multiplyByLocalCoordinateTransform(originPoint, globe); + modelview.multiplyMatrix(origin); + + result.position.copy(originPos); + result.range = -modelview[11]; + result.heading = modelview.extractHeading(this.roll); // disambiguate heading and roll + result.tilt = modelview.extractTilt(); + result.roll = this.roll; // roll passes straight through + + return result; + }; + + /** + * Returns a string representation of this object. + * @returns {String} + */ + Camera.prototype.toString = function () { + return this.position.toString() + "," + this.heading + "\u00b0," + this.tilt + "\u00b0," + this.roll + "\u00b0"; + }; + + return Camera; + }); + diff --git a/src/geom/LookAt.js b/src/geom/LookAt.js new file mode 100644 index 000000000..f023c15fb --- /dev/null +++ b/src/geom/LookAt.js @@ -0,0 +1,149 @@ +/* + * Copyright 2015-2017 WorldWind Contributors + * + * 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 LookAt + */ +define([ + '../error/ArgumentError', + '../util/Logger', + '../geom/Matrix', + '../geom/Position' + ], + function (ArgumentError, + Logger, + Matrix, + Position) { + "use strict"; + + var LookAt = function () { + /** + * The geographic position at the center of the viewport. + * @type {Location} + */ + this.position = new Position(30, -110, 0); + + /** + * Look at heading, in degrees clockwise from north. + * @type {Number} + * @default 0 + */ + this.heading = 0; + + /** + * Look at tilt, in degrees. + * @type {Number} + * @default 0 + */ + this.tilt = 0; + + /** + * Look at roll, in degrees. + * @type {Number} + * @default 0 + */ + this.roll = 0; + + /** + * The distance from the eye point to its look at location. + * @type {Number} + * @default 10,000 kilometers + */ + this.range = 10e6; // TODO: Compute initial range to fit globe in viewport. + }; + + /** + * Internal use only. + * Computes the model view matrix for this look at view. + * @ignore + */ + LookAt.prototype.computeViewingTransform = function (globe, modelview) { + if (!globe) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "LookAt", "computeViewingTransform", "missingGlobe")); + } + + if (!modelview) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "LookAt", "computeViewingTransform", "missingModelview")); + } + + modelview.setToIdentity(); + modelview.multiplyByLookAtModelview(this.position, this.range, this.heading, this.tilt, this.roll, globe); + + return modelview; + }; + + /** + * Indicates whether the components of this object are equal to those of a specified object. + * @param {LookAt} otherLookAt The object to test equality with. May be null or undefined, in which case this + * function returns false. + * @returns {boolean} true if all components of this object are equal to the corresponding + * components of the specified object, otherwise false. + */ + LookAt.prototype.equals = function (otherLookAt) { + if (otherLookAt) { + return this.position.equals(otherLookAt.position) && + this.heading === otherLookAt.heading && + this.tilt === otherLookAt.tilt && + this.roll === otherLookAt.roll && + this.range === otherLookAt.range; + } + + return false; + }; + + /** + * Creates a new object that is a copy of this object. + * @returns {LookAt} The new object. + */ + LookAt.prototype.clone = function () { + var clone = new LookAt(); + clone.copy(this); + + return clone; + }; + + /** + * Copies the components of a specified object to this object. + * @param {LookAt} copyObject The object to copy. + * @returns {LookAt} A copy of this object equal to copyObject. + * @throws {ArgumentError} If the specified object is null or undefined. + */ + LookAt.prototype.copy = function (copyObject) { + if (!copyObject) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "LookAt", "copy", "missingObject")); + } + + this.position.copy(copyObject.position); + this.heading = copyObject.heading; + this.tilt = copyObject.tilt; + this.roll = copyObject.roll; + this.range = copyObject.range; + + return this; + }; + + /** + * Returns a string representation of this object. + * @returns {String} + */ + LookAt.prototype.toString = function () { + return this.position.toString() + "," + this.heading + "\u00b0," + this.tilt + "\u00b0," + this.roll + "\u00b0"; + }; + + return LookAt; + }); \ No newline at end of file diff --git a/src/geom/Matrix.js b/src/geom/Matrix.js index cf38ba19d..ed34ad58b 100644 --- a/src/geom/Matrix.js +++ b/src/geom/Matrix.js @@ -954,12 +954,13 @@ define([ * * @param {Number} viewportWidth The viewport width, in screen coordinates. * @param {Number} viewportHeight The viewport height, in screen coordinates. + * @param {Number} fovyDegrees The camera vertical field of view. * @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) { + Matrix.prototype.setToPerspectiveProjection = function (viewportWidth, viewportHeight, fovyDegrees, nearDistance, farDistance) { if (viewportWidth <= 0) { throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "setToPerspectiveProjection", "invalidWidth")); @@ -970,6 +971,11 @@ define([ "invalidHeight")); } + if (fovyDegrees <= 0 || fovyDegrees >= 180) { + throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "setToPerspectiveProjection", + "invalidFieldOfView")); + } + if (nearDistance === farDistance) { throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Matrix", "setToPerspectiveProjection", "Near and far distance are the same.")); @@ -980,24 +986,23 @@ define([ "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(); + // Compute the dimensions of the near rectangle given the specified parameters. + var aspect = viewportWidth / viewportHeight, + tanfovy_2 = Math.tan(fovyDegrees * 0.5 / 180.0 * Math.PI), + nearHeight = 2 * nearDistance * tanfovy_2, + nearWidth = nearHeight * aspect; // Taken from Mathematics for 3D Game Programming and Computer Graphics, Second Edition, equation 4.52. // Row 1 - this[0] = 2 * nearDistance / (right - left); + this[0] = 2 * nearDistance / nearWidth; this[1] = 0; - this[2] = (right + left) / (right - left); + this[2] = 0; this[3] = 0; // Row 2 this[4] = 0; - this[5] = 2 * nearDistance / (top - bottom); - this[6] = (top + bottom) / (top - bottom); + this[5] = 2 * nearDistance / nearHeight; + this[6] = 0; this[7] = 0; // Row 3 this[8] = 0; @@ -1108,6 +1113,39 @@ define([ return result; }; + /** + * Returns this viewing matrix's heading angle in degrees. The roll argument enables the caller to disambiguate + * heading and roll when the two rotation axes for heading and roll are parallel, causing gimbal lock. + *

+ * The result of this method is undefined if this matrix is not a viewing matrix. + * + * @param {Number} roll the viewing matrix's roll angle in degrees, or 0 if the roll angle is unknown + * + * @return {Number} the extracted heading angle in degrees + */ + Matrix.prototype.extractHeading = function (roll) { + var rad = roll * Angle.DEGREES_TO_RADIANS; + var cr = Math.cos(rad); + var sr = Math.sin(rad); + + var ch = (cr * this[0]) - (sr * this[4]); + var sh = (sr * this[5]) - (cr * this[1]); + return Math.atan2(sh, ch) * Angle.RADIANS_TO_DEGREES; + }; + + /** + * Returns this viewing matrix's tilt angle in degrees. + *

+ * The result of this method is undefined if this matrix is not a viewing matrix. + * + * @return {Number} the extracted heading angle in degrees + */ + Matrix.prototype.extractTilt = function () { + var ct = this[10]; + var st = Math.sqrt(this[2] * this[2] + this[6] * this[6]); + return Math.atan2(st, ct) * Angle.RADIANS_TO_DEGREES; + }; + /** * Returns this viewing matrix's forward vector. *

diff --git a/src/layer/ViewControlsLayer.js b/src/layer/ViewControlsLayer.js index 98434ff11..631d2f6cf 100644 --- a/src/layer/ViewControlsLayer.js +++ b/src/layer/ViewControlsLayer.js @@ -34,6 +34,7 @@ define([ '../layer/Layer', '../geom/Location', '../util/Logger', + '../geom/LookAt', '../util/Offset', '../shapes/ScreenImage', '../geom/Vec2' @@ -43,6 +44,7 @@ define([ Layer, Location, Logger, + LookAt, Offset, ScreenImage, Vec2) { @@ -162,9 +164,9 @@ define([ /** * The incremental amount to narrow or widen the field of view each cycle, in degrees. * @type {Number} - * @default 0.1 + * @default 0.5 */ - this.fieldOfViewIncrement = 0.1; + this.fieldOfViewIncrement = 0.5; /** * The scale factor governing the pan speed. Increased values cause faster panning. @@ -232,6 +234,13 @@ define([ // Establish event handlers. this.wwd.worldWindowController.addGestureListener(this); + + /** + * Internal use only. + * The current state of the viewing parameters during an operation as a look at view. + * @ignore + */ + this.lookAt = new LookAt(); }; ViewControlsLayer.prototype = Object.create(Layer.prototype); @@ -561,9 +570,11 @@ define([ this.activeOperation = null; e.preventDefault(); } else { + var requestRedraw = false; // Perform the active operation, or determine it and then perform it. if (this.activeOperation) { handled = this.activeOperation.call(this, e, null); + requestRedraw = true; e.preventDefault(); } else { topObject = this.pickControl(e); @@ -571,13 +582,16 @@ define([ var operation = this.determineOperation(e, topObject); if (operation) { handled = operation.call(this, e, topObject); + requestRedraw = true; } } } // Determine and display the new highlight state. - this.handleHighlight(e, topObject); - this.wwd.redraw(); + var highlighted = this.handleHighlight(e, topObject); + if (requestRedraw || highlighted) { + this.wwd.redraw(); + } } return handled; @@ -683,6 +697,7 @@ define([ if (this.isPointerDown(e) || this.isTouchStart(e)) { this.activeControl = control; this.activeOperation = this.handlePan; + this.wwd.camera.getAsLookAt(this.lookAt); e.preventDefault(); if (this.isTouchStart(e)) { @@ -696,16 +711,18 @@ define([ var dx = thisLayer.panControlCenter[0] - thisLayer.currentEventPoint[0], dy = thisLayer.panControlCenter[1] - (thisLayer.wwd.viewport.height - thisLayer.currentEventPoint[1]), - oldLat = thisLayer.wwd.navigator.lookAtLocation.latitude, - oldLon = thisLayer.wwd.navigator.lookAtLocation.longitude, + lookAt = thisLayer.lookAt, + oldLat = lookAt.position.latitude, + oldLon = lookAt.position.longitude, // Scale the increment by a constant and the relative distance of the eye to the surface. scale = thisLayer.panIncrement - * (thisLayer.wwd.navigator.range / thisLayer.wwd.globe.radiusAt(oldLat, oldLon)), - heading = thisLayer.wwd.navigator.heading + (Math.atan2(dx, dy) * Angle.RADIANS_TO_DEGREES), + * (lookAt.range / thisLayer.wwd.globe.radiusAt(oldLat, oldLon)), + heading = lookAt.heading + (Math.atan2(dx, dy) * Angle.RADIANS_TO_DEGREES), distance = scale * Math.sqrt(dx * dx + dy * dy); - Location.greatCircleLocation(thisLayer.wwd.navigator.lookAtLocation, heading, -distance, - thisLayer.wwd.navigator.lookAtLocation); + Location.greatCircleLocation(lookAt.position, heading, -distance, + lookAt.position); + thisLayer.wwd.camera.setFromLookAt(lookAt); thisLayer.wwd.redraw(); setTimeout(setLookAtLocation, 50); } @@ -726,6 +743,7 @@ define([ if (this.isPointerDown(e) || this.isTouchStart(e)) { this.activeControl = control; this.activeOperation = this.handleZoom; + this.wwd.camera.getAsLookAt(this.lookAt); e.preventDefault(); if (this.isTouchStart(e)) { @@ -736,11 +754,13 @@ define([ var thisLayer = this; // capture 'this' for use in the function var setRange = function () { if (thisLayer.activeControl) { + var lookAt = thisLayer.lookAt; if (thisLayer.activeControl === thisLayer.zoomInControl) { - thisLayer.wwd.navigator.range *= (1 - thisLayer.zoomIncrement); + lookAt.range *= (1 - thisLayer.zoomIncrement); } else if (thisLayer.activeControl === thisLayer.zoomOutControl) { - thisLayer.wwd.navigator.range *= (1 + thisLayer.zoomIncrement); + lookAt.range *= (1 + thisLayer.zoomIncrement); } + thisLayer.wwd.camera.setFromLookAt(lookAt); thisLayer.wwd.redraw(); setTimeout(setRange, 50); } @@ -761,6 +781,7 @@ define([ if (this.isPointerDown(e) || this.isTouchStart(e)) { this.activeControl = control; this.activeOperation = this.handleHeading; + this.wwd.camera.getAsLookAt(this.lookAt); e.preventDefault(); if (this.isTouchStart(e)) { @@ -769,18 +790,20 @@ define([ // This function is called by the timer to perform the operation. var thisLayer = this; // capture 'this' for use in the function - var setRange = function () { + var setHeading = function () { + var lookAt = thisLayer.lookAt; if (thisLayer.activeControl) { if (thisLayer.activeControl === thisLayer.headingLeftControl) { - thisLayer.wwd.navigator.heading += thisLayer.headingIncrement; + lookAt.heading += thisLayer.headingIncrement; } else if (thisLayer.activeControl === thisLayer.headingRightControl) { - thisLayer.wwd.navigator.heading -= thisLayer.headingIncrement; + lookAt.heading -= thisLayer.headingIncrement; } + thisLayer.wwd.camera.setFromLookAt(lookAt); thisLayer.wwd.redraw(); - setTimeout(setRange, 50); + setTimeout(setHeading, 50); } }; - setTimeout(setRange, 50); + setTimeout(setHeading, 50); handled = true; } @@ -795,6 +818,7 @@ define([ if (this.isPointerDown(e) || this.isTouchStart(e)) { this.activeControl = control; this.activeOperation = this.handleTilt; + this.wwd.camera.getAsLookAt(this.lookAt); e.preventDefault(); if (this.isTouchStart(e)) { @@ -803,20 +827,22 @@ define([ // This function is called by the timer to perform the operation. var thisLayer = this; // capture 'this' for use in the function - var setRange = function () { + var setTilt = function () { if (thisLayer.activeControl) { + var lookAt = thisLayer.lookAt; if (thisLayer.activeControl === thisLayer.tiltUpControl) { - thisLayer.wwd.navigator.tilt = - Math.max(0, thisLayer.wwd.navigator.tilt - thisLayer.tiltIncrement); + lookAt.tilt = + Math.max(0, lookAt.tilt - thisLayer.tiltIncrement); } else if (thisLayer.activeControl === thisLayer.tiltDownControl) { - thisLayer.wwd.navigator.tilt = - Math.min(90, thisLayer.wwd.navigator.tilt + thisLayer.tiltIncrement); + lookAt.tilt = + Math.min(90, lookAt.tilt + thisLayer.tiltIncrement); } + thisLayer.wwd.camera.setFromLookAt(lookAt); thisLayer.wwd.redraw(); - setTimeout(setRange, 50); + setTimeout(setTilt, 50); } }; - setTimeout(setRange, 50); + setTimeout(setTilt, 50); handled = true; } @@ -876,20 +902,20 @@ define([ // This function is called by the timer to perform the operation. var thisLayer = this; // capture 'this' for use in the function - var setRange = function () { + var setFov = function () { if (thisLayer.activeControl) { if (thisLayer.activeControl === thisLayer.fovWideControl) { - thisLayer.wwd.navigator.fieldOfView = - Math.max(90, thisLayer.wwd.navigator.fieldOfView + thisLayer.fieldOfViewIncrement); + thisLayer.wwd.camera.fieldOfView = + Math.min(90, thisLayer.wwd.camera.fieldOfView + thisLayer.fieldOfViewIncrement); } else if (thisLayer.activeControl === thisLayer.fovNarrowControl) { - thisLayer.wwd.navigator.fieldOfView = - Math.min(0, thisLayer.wwd.navigator.fieldOfView - thisLayer.fieldOfViewIncrement); + thisLayer.wwd.camera.fieldOfView = + Math.max(0, thisLayer.wwd.camera.fieldOfView - thisLayer.fieldOfViewIncrement); } thisLayer.wwd.redraw(); - setTimeout(setRange, 50); + setTimeout(setFov, 50); } }; - setTimeout(setRange, 50); + setTimeout(setFov, 50); handled = true; } @@ -901,10 +927,14 @@ define([ if (this.activeControl) { // Highlight the active control. this.highlight(this.activeControl, true); + return true; } else if (topObject && this.isControl(topObject)) { // Highlight the control under the cursor or finger. this.highlight(topObject, true); + return true; } + + return false; }; // Intentionally not documented. Sets the highlight state of a control. diff --git a/src/navigate/LookAtNavigator.js b/src/navigate/LookAtNavigator.js index efa1678a0..dfe80798c 100644 --- a/src/navigate/LookAtNavigator.js +++ b/src/navigate/LookAtNavigator.js @@ -30,41 +30,81 @@ */ define([ '../geom/Location', - '../navigate/Navigator', + '../geom/LookAt', + '../navigate/LookAtPositionProxy', + '../navigate/Navigator' ], function (Location, + LookAt, + LookAtPositionProxy, Navigator) { "use strict"; /** * Constructs a look-at navigator. + * @deprecated * @alias LookAtNavigator * @constructor * @augments Navigator * @classdesc Represents a navigator containing the required variables to enable the user to pan, zoom and tilt - * the globe. + * the globe. Deprecated, see {@Link LookAt}. */ - var LookAtNavigator = function () { - Navigator.call(this); + var LookAtNavigator = function (worldWindow) { + Navigator.call(this, worldWindow); + + /** + * Internal use only. + * A temp variable used to hold the position during calculations and property retrieval. Using an object + * level temp property negates the need for ad-hoc allocations and reduces load on the garbage collector. + * @ignore + */ + this.scratchLookAtPositionProxy = new LookAtPositionProxy(this); + }; + + LookAtNavigator.prototype = Object.create(Navigator.prototype); + + Object.defineProperties(LookAtNavigator.prototype, { /** * The geographic location at the center of the viewport. * @type {Location} */ - this.lookAtLocation = new Location(30, -110); + lookAtLocation: { + get: function () { + this.wwd.camera.getAsLookAt(this.scratchLookAt); + this.scratchLookAtPositionProxy.position.copy(this.scratchLookAt.position); + return this.scratchLookAtPositionProxy; + }, + set: function (value) { + var lookAt = this.wwd.camera.getAsLookAt(this.scratchLookAt); + lookAt.position.latitude = value.latitude; + lookAt.position.longitude = value.longitude; + if (value.altitude) { + lookAt.position.altitude = value.altitude; + } + else { + lookAt.position.altitude = 0; + } + this.wwd.camera.setFromLookAt(lookAt); + } + }, /** * The distance from this navigator's eye point to its look-at location. * @type {Number} * @default 10,000 kilometers */ - this.range = 10e6; // TODO: Compute initial range to fit globe in viewport. - - // Development testing only. Set this to false to suppress default navigator limits on 2D globes. - this.enable2DLimits = true; - }; - - LookAtNavigator.prototype = Object.create(Navigator.prototype); + range: { + get: function () { + return this.wwd.camera.getAsLookAt(this.scratchLookAt).range; + }, + set: function (value) { + var lookAt = this.wwd.camera.getAsLookAt(this.scratchLookAt); + lookAt.range = value; + this.wwd.camera.setFromLookAt(lookAt); + } + } + }); return LookAtNavigator; }); \ No newline at end of file diff --git a/src/navigate/LookAtPositionProxy.js b/src/navigate/LookAtPositionProxy.js new file mode 100644 index 000000000..cae07c740 --- /dev/null +++ b/src/navigate/LookAtPositionProxy.js @@ -0,0 +1,70 @@ +/* + * Copyright 2015-2017 WorldWind Contributors + * + * 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 LookAtPositionProxy + */ +define([ + '../geom/Position' + ], + function (Position) { + "use strict"; + + /** + * Constructs a look-at position proxy + * @deprecated + * @alias LookAtPositionProxy + * @constructor + * @classdesc A Position proxy class that is used for backward compatibility purposes by the deprecated LookAtNavigator class. + */ + var LookAtPositionProxy = function (navigator) { + this.position = new Position(0, 0, 0); + this.lookAtNavigator = navigator; + }; + + Object.defineProperties(LookAtPositionProxy.prototype, { + latitude: { + get: function () { + return this.position.latitude; + }, + set: function (value) { + this.position.latitude = value; + this.lookAtNavigator.lookAtLocation = this.position; + } + }, + + longitude: { + get: function () { + return this.position.longitude; + }, + set: function (value) { + this.position.longitude = value; + this.lookAtNavigator.lookAtLocation = this.position; + } + }, + + altitude: { + get: function () { + return this.position.altitude; + }, + set: function (value) { + this.position.altitude = value; + this.lookAtNavigator.lookAtLocation = this.position; + } + } + }); + + return LookAtPositionProxy; + }); \ No newline at end of file diff --git a/src/navigate/Navigator.js b/src/navigate/Navigator.js index abe948a1f..8319bf5c4 100644 --- a/src/navigate/Navigator.js +++ b/src/navigate/Navigator.js @@ -28,39 +28,90 @@ /** * @exports Navigator */ -define([], - function () { +define(['../error/ArgumentError', + '../util/Logger', + '../geom/LookAt' + ], + function (ArgumentError, + Logger, + LookAt) { "use strict"; /** * Constructs a base navigator. + * @deprecated * @alias Navigator * @constructor * @classdesc Provides an abstract base class for navigators. This class is not meant to be instantiated - * directly. See {@Link LookAtNavigator} for a concrete navigator. + * directly. Deprecated, see {@Link Camera}. + * @param {WorldWindow} worldWindow The WorldWindow to associate with this navigator. */ - var Navigator = function () { + var Navigator = function (worldWindow) { + if (!worldWindow) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Navigator", "constructor", "missingWorldWindow")); + } + + this.wwd = worldWindow; + + /** + * Internal use only. + * A temp variable used to hold the look view during calculations. Using an object level temp property + * negates the need for ad-hoc allocations and reduces load on the garbage collector. + * @ignore + */ + this.scratchLookAt = new LookAt(); + }; + + Object.defineProperties(Navigator.prototype, { /** * This navigator's heading, in degrees clockwise from north. * @type {Number} * @default 0 */ - this.heading = 0; + heading: { + get: function () { + return this.wwd.camera.getAsLookAt(this.scratchLookAt).heading; + }, + set: function (value) { + var lookAt = this.wwd.camera.getAsLookAt(this.scratchLookAt); + lookAt.heading = value; + this.wwd.camera.setFromLookAt(lookAt); + } + }, /** * This navigator's tilt, in degrees. * @type {Number} * @default 0 */ - this.tilt = 0; + tilt: { + get: function () { + return this.wwd.camera.getAsLookAt(this.scratchLookAt).tilt; + }, + set: function (value) { + var lookAt = this.wwd.camera.getAsLookAt(this.scratchLookAt); + lookAt.tilt = value; + this.wwd.camera.setFromLookAt(lookAt); + } + }, /** * This navigator's roll, in degrees. * @type {Number} * @default 0 */ - this.roll = 0; - }; + roll: { + get: function () { + return this.wwd.camera.getAsLookAt(this.scratchLookAt).roll; + }, + set: function (value) { + var lookAt = this.wwd.camera.getAsLookAt(this.scratchLookAt); + lookAt.roll = value; + this.wwd.camera.setFromLookAt(lookAt); + } + } + }); return Navigator; }); \ No newline at end of file diff --git a/src/render/DrawContext.js b/src/render/DrawContext.js index 39c7d5cae..6f7d4559a 100644 --- a/src/render/DrawContext.js +++ b/src/render/DrawContext.js @@ -374,9 +374,22 @@ define([ // Intentionally not documented. this.pixelScale = 1; - // TODO: replace with camera in the next phase of navigator refactoring + /** + * The deprecated navigator that can be used to manipulate the globe. See the {@link Camera} and {@link LookAt} + * classes for replacement functionality. + * @deprecated + * @type {LookAtNavigator} + * @default [LookAtNavigator]{@link LookAtNavigator} + */ this.navigator = null; + /** + * The viewing point as a camera. + * @type {Camera} + * @readonly + */ + this.camera = null; + /** * The model-view matrix. The model-view matrix transforms points from model coordinates to eye * coordinates. @@ -428,9 +441,6 @@ define([ // Intentionally not documented. this.pixelSizeFactor = 0; - // Intentionally not documented. - this.pixelSizeOffset = 0; - // Intentionally not documented. this.glExtensionsCache = {}; }; @@ -469,6 +479,7 @@ define([ this.verticalExaggeration = 1; this.frameStatistics = null; this.accumulateOrderedRenderables = true; + this.pixelSizeFactor = 0; // Reset picking properties that may be set by the WorldWindow. this.pickingMode = false; @@ -1520,18 +1531,13 @@ define([ * coordinates per pixel. */ DrawContext.prototype.pixelSizeAtDistance = function (distance) { - // Compute the pixel size from the width of a rectangle carved out of the frustum in model coordinates at - // the specified distance along the -Z axis and the viewport width in screen coordinates. The pixel size is - // expressed in model coordinates per screen coordinate (e.g. meters per pixel). - // - // The frustum width is determined by noticing that the frustum size is a linear function of distance from - // the eye point. The linear equation constants are determined during initialization, then solved for - // distance here. - // - // This considers only the frustum width by assuming that the frustum and viewport share the same aspect - // ratio, so that using either the frustum width or height results in the same pixel size. - - return this.pixelSizeFactor * distance + this.pixelSizeOffset; + if (this.pixelSizeFactor === 0) { // cache the scaling factor used to convert distances to pixel sizes + var fovyDegrees = this.camera.fieldOfView; + var tanfovy_2 = Math.tan(fovyDegrees * 0.5 / 180.0 * Math.PI); + this.pixelSizeFactor = 2 * tanfovy_2 / this.viewport.height; + } + + return distance * this.pixelSizeFactor; }; /** diff --git a/src/shapes/AbstractShape.js b/src/shapes/AbstractShape.js index 5bcf2ce2c..ff6d72d93 100644 --- a/src/shapes/AbstractShape.js +++ b/src/shapes/AbstractShape.js @@ -422,7 +422,7 @@ define([ }; /** - * Apply the current navigator's model-view-projection matrix. + * Apply the current camera's model-view-projection matrix. * @param {DrawContext} dc The current draw context. * @protected */ @@ -433,7 +433,7 @@ define([ }; /** - * Apply the current navigator's model-view-projection matrix with an offset to make this shape's outline + * Apply the current camera's model-view-projection matrix with an offset to make this shape's outline * stand out. * @param {DrawContext} dc The current draw context. * @protected diff --git a/src/shapes/Compass.js b/src/shapes/Compass.js index d9bedfe96..ff286a7e9 100644 --- a/src/shapes/Compass.js +++ b/src/shapes/Compass.js @@ -46,7 +46,7 @@ define([ * @constructor * @augments ScreenImage * @classdesc Displays a compass image at a specified location in the WorldWindow. The compass image rotates - * and tilts to reflect the current navigator's heading and tilt. + * and tilts to reflect the current camera's heading and tilt. * @param {Offset} screenOffset The offset indicating the image's placement on the screen. If null or undefined * the compass is placed at the upper-right corner of the WorldWindow. * Use [the image offset property]{@link ScreenImage#imageOffset} to position the image relative to the @@ -79,13 +79,13 @@ define([ Compass.prototype = Object.create(ScreenImage.prototype); /** - * Capture the navigator's heading and tilt and apply it to the compass' screen image. + * Capture the camera's heading and tilt and apply it to the compass' screen image. * @param {DrawContext} dc The current draw context. */ Compass.prototype.render = function (dc) { - // Capture the navigator's heading and tilt and apply it to the compass' screen image. - this.imageRotation = dc.navigator.heading; - this.imageTilt = dc.navigator.tilt; + // Capture the camera's heading and tilt and apply it to the compass' screen image. + this.imageRotation = dc.camera.heading; + this.imageTilt = dc.camera.tilt; var t = this.getActiveTexture(dc); if (t) { diff --git a/src/shapes/Placemark.js b/src/shapes/Placemark.js index ee38d37c1..fe7283612 100644 --- a/src/shapes/Placemark.js +++ b/src/shapes/Placemark.js @@ -712,7 +712,7 @@ define([ Placemark.matrix.multiplyMatrix(this.imageTransform); var actualRotation = this.imageRotationReference === WorldWind.RELATIVE_TO_GLOBE ? - dc.navigator.heading - this.imageRotation : -this.imageRotation; + dc.camera.heading - this.imageRotation : -this.imageRotation; Placemark.matrix.multiplyByTranslation(0.5, 0.5, 0); Placemark.matrix.multiplyByRotation(0, 0, 1, actualRotation); Placemark.matrix.multiplyByTranslation(-0.5, -0.5, 0); @@ -720,7 +720,7 @@ define([ // Perform the tilt before applying the rotation so that the image tilts back from its base into // the view volume. var actualTilt = this.imageTiltReference === WorldWind.RELATIVE_TO_GLOBE ? - dc.navigator.tilt + this.imageTilt : this.imageTilt; + dc.camera.tilt + this.imageTilt : this.imageTilt; Placemark.matrix.multiplyByRotation(-1, 0, 0, actualTilt); program.loadModelviewProjection(gl, Placemark.matrix); diff --git a/src/util/GoToAnimator.js b/src/util/GoToAnimator.js index f51bbf371..3feced89b 100644 --- a/src/util/GoToAnimator.js +++ b/src/util/GoToAnimator.js @@ -31,11 +31,13 @@ define([ '../geom/Location', '../util/Logger', + '../geom/LookAt', '../geom/Position', '../geom/Vec3' ], function (Location, Logger, + LookAt, Position, Vec3) { "use strict"; @@ -43,7 +45,7 @@ define([ * Constructs a GoTo animator. * @alias GoToAnimator * @constructor - * @classdesc Incrementally and smoothly moves a {@link Navigator} to a specified position. + * @classdesc Incrementally and smoothly moves the {@link Camera} to a specified position. * @param {WorldWindow} worldWindow The WorldWindow in which to perform the animation. * @throws {ArgumentError} If the specified WorldWindow is null or undefined. */ @@ -84,6 +86,14 @@ define([ * @readonly */ this.cancelled = false; + + /** + * Internal use only. + * A temp variable used to hold the current view as a look at during calculations. Using an object level temp property + * negates the need for ad-hoc allocations and reduces load on the garbage collector. + * @ignore + */ + this.lookAt = new LookAt(); }; // Stop the current animation. @@ -92,10 +102,10 @@ define([ }; /** - * Moves the navigator to a specified location or position. - * @param {Location | Position} position The location or position to move the navigator to. If this + * Moves the camera to a specified look at location or position. + * @param {Location | Position} position The location or position to move the camera 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. + * at the specified altitude. Otherwise the end point is at the current altitude of the camera. * @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. @@ -111,15 +121,16 @@ define([ // Reset the cancellation flag. this.cancelled = false; + this.wwd.camera.getAsLookAt(this.lookAt); // Capture the target position and determine its altitude. this.targetPosition = new Position(position.latitude, position.longitude, - position.altitude || this.wwd.navigator.range); + position.altitude || this.lookAt.range); // Capture the start position and start time. this.startPosition = new Position( - this.wwd.navigator.lookAtLocation.latitude, - this.wwd.navigator.lookAtLocation.longitude, - this.wwd.navigator.range); + this.lookAt.position.latitude, + this.lookAt.position.longitude, + this.lookAt.range); this.startTime = Date.now(); // Determination of the pan and range velocities requires the distance to be travelled. @@ -149,7 +160,7 @@ define([ // We need to capture the time the max altitude is reached in order to begin decreasing the range // midway through the animation. If we're already above the max altitude, then that time is now since // we don't back out if the current altitude is above the computed max altitude. - this.maxAltitudeReachedTime = this.maxAltitude <= this.wwd.navigator.range ? Date.now() : null; + this.maxAltitudeReachedTime = this.maxAltitude <= this.lookAt.range ? Date.now() : null; // Compute the total range to travel since we need that to compute the range velocity. // Note that the range velocity and pan velocity are computed so that the respective animations, which @@ -206,9 +217,9 @@ define([ // This is the timer callback function. It invokes the range animator and the pan animator. var currentPosition = new Position( - this.wwd.navigator.lookAtLocation.latitude, - this.wwd.navigator.lookAtLocation.longitude, - this.wwd.navigator.range); + this.lookAt.position.latitude, + this.lookAt.position.longitude, + this.lookAt.range); var continueAnimation = this.updateRange(currentPosition); continueAnimation = this.updateLocation(currentPosition) || continueAnimation; @@ -230,10 +241,10 @@ define([ elapsedTime = Date.now() - this.startTime; nextRange = Math.min(this.startPosition.altitude + this.rangeVelocity * elapsedTime, this.maxAltitude); // We're done if we get withing 1 meter of the desired range. - if (Math.abs(this.wwd.navigator.range - nextRange) < 1) { + if (Math.abs(this.lookAt.range - nextRange) < 1) { this.maxAltitudeReachedTime = Date.now(); } - this.wwd.navigator.range = nextRange; + this.lookAt.range = nextRange; continueAnimation = true; } else { elapsedTime = Date.now() - this.maxAltitudeReachedTime; @@ -244,11 +255,13 @@ define([ nextRange = this.maxAltitude + (this.rangeVelocity * elapsedTime); nextRange = Math.min(nextRange, this.targetPosition.altitude); } - this.wwd.navigator.range = nextRange; + this.lookAt.range = nextRange; // We're done if we get withing 1 meter of the desired range. - continueAnimation = Math.abs(this.wwd.navigator.range - this.targetPosition.altitude) > 1; + continueAnimation = Math.abs(this.lookAt.range - this.targetPosition.altitude) > 1; } + this.wwd.camera.setFromLookAt(this.lookAt); + return continueAnimation; }; @@ -265,8 +278,9 @@ define([ new Location(0, 0)), locationReached = false; - this.wwd.navigator.lookAtLocation.latitude = nextLocation.latitude; - this.wwd.navigator.lookAtLocation.longitude = nextLocation.longitude; + this.lookAt.position.latitude = nextLocation.latitude; + this.lookAt.position.longitude = nextLocation.longitude; + this.wwd.camera.setFromLookAt(this.lookAt); // We're done if we're within a meter of the desired location. if (nextDistance < 1 / this.wwd.globe.equatorialRadius) { diff --git a/src/util/KeyboardControls.js b/src/util/KeyboardControls.js index fe2090988..9e178594e 100644 --- a/src/util/KeyboardControls.js +++ b/src/util/KeyboardControls.js @@ -14,13 +14,14 @@ * @@author Bruce Schubert */ define([ - '../geom/Location'], + '../geom/Location', + '../geom/LookAt'], function ( - Location) { + Location, LookAt) { "use strict"; /** * Creates a KeyboardController that dispatches keystrokes from the - * WorldWindow to the Navigator. Note: the WorldWindow's canvas must be focusable; + * WorldWindow to the Camera. Note: the WorldWindow's canvas must be focusable; * this can be accomplished by establishing the "tabindex" on the canvas element. * @param {WorldWindow} wwd The keyboard event generator. * @returns {KeyboardControls} @@ -61,6 +62,13 @@ define([ */ this.panIncrement = 0.0000000005; + /** + * Internal use only. + * The current state of the viewing parameters during an operation as a look at view. + * @ignore + */ + this.lookAt = new LookAt(); + }; /** @@ -112,7 +120,9 @@ define([ * Reset the view to North up. */ KeyboardControls.prototype.resetHeading = function () { - this.wwd.navigator.heading = Number(0); + this.wwd.camera.getAsLookAt(this.lookAt); + this.lookAt.heading = Number(0); + this.wwd.camera.setFromLookAt(this.lookAt); this.wwd.redraw(); }; @@ -120,17 +130,11 @@ define([ * Reset the view to North up and nadir. */ KeyboardControls.prototype.resetHeadingAndTilt = function () { - this.wwd.navigator.heading = 0; - this.wwd.navigator.tilt = 0; - this.wwd.redraw(); // calls applyLimits which may change the location - -// // Tilting the view will change the location due to a deficiency in -// // the early release of WW. So we set the location to the center of the -// // current crosshairs position (viewpoint) to resolve this issue -// var viewpoint = this.getViewpoint(), -// lat = viewpoint.target.latitude, -// lon = viewpoint.target.longitude; -// this.lookAt(lat, lon); + this.wwd.camera.getAsLookAt(this.lookAt); + this.lookAt.heading = 0; + this.lookAt.tilt = 0; + this.wwd.camera.setFromLookAt(this.lookAt); + this.wwd.redraw(); }; /** @@ -150,16 +154,18 @@ define([ */ KeyboardControls.prototype.handleZoom = function (operation) { this.activeOperation = this.handleZoom; + this.wwd.camera.getAsLookAt(this.lookAt); // This function is called by the timer to perform the operation. var self = this, // capture 'this' for use in the function setRange = function () { if (self.activeOperation) { if (operation === "zoomIn") { - self.wwd.navigator.range *= (1 - self.zoomIncrement); + self.lookAt.range *= (1 - self.zoomIncrement); } else if (operation === "zoomOut") { - self.wwd.navigator.range *= (1 + self.zoomIncrement); + self.lookAt.range *= (1 + self.zoomIncrement); } + self.wwd.camera.setFromLookAt(self.lookAt); self.wwd.redraw(); setTimeout(setRange, 50); } @@ -173,13 +179,14 @@ define([ */ KeyboardControls.prototype.handlePan = function (operation) { this.activeOperation = this.handlePan; + this.wwd.camera.getAsLookAt(this.lookAt); // This function is called by the timer to perform the operation. var self = this, // capture 'this' for use in the function setLookAtLocation = function () { if (self.activeOperation) { - var heading = self.wwd.navigator.heading, - distance = self.panIncrement * self.wwd.navigator.range; + var heading = self.lookAt.heading, + distance = self.panIncrement * self.lookAt.range; switch (operation) { case 'panUp' : @@ -194,12 +201,13 @@ define([ heading += 90; break; } - // Update the navigator's lookAtLocation + // Update the cameras's lookAt Position Location.greatCircleLocation( - self.wwd.navigator.lookAtLocation, + self.lookAt.position, heading, distance, - self.wwd.navigator.lookAtLocation); + self.lookAt.position); + self.wwd.camera.setFromLookAt(self.lookAt); self.wwd.redraw(); setTimeout(setLookAtLocation, 50); } diff --git a/src/util/WWMath.js b/src/util/WWMath.js index 455674892..8115b5214 100644 --- a/src/util/WWMath.js +++ b/src/util/WWMath.js @@ -664,23 +664,17 @@ define([ * Matrix.setToPerspectiveProjection. The given distance should specify the smallest distance between the * eye and the object being viewed, but may be an approximation if an exact distance is not required. * - * @param {Number} viewportWidth The viewport width, in screen coordinates. - * @param {Number} viewportHeight The viewport height, in screen coordinates. + * @param {Number} fovyDegrees The camera vertical field of view. * @param {Number} distanceToSurface The distance from the perspective eye point to the nearest object, in * meters. * @returns {Number} The maximum near clip distance, in meters. * @throws {ArgumentError} If the specified width or height is less than or equal to zero, or if the * specified distance is negative. */ - perspectiveNearDistance: function (viewportWidth, viewportHeight, distanceToSurface) { - if (viewportWidth <= 0) { + perspectiveNearDistance: function (fovyDegrees, distanceToSurface) { + if (fovyDegrees <= 0 || fovyDegrees >= 180) { throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WWMath", "perspectiveNearDistance", - "invalidWidth")); - } - - if (viewportHeight <= 0) { - throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WWMath", "perspectiveNearDistance", - "invalidHeight")); + "invalidFieldOfView")); } if (distanceToSurface < 0) { @@ -688,29 +682,8 @@ define([ "The specified distance is negative.")); } - // Compute the maximum near clip distance that avoids clipping an object at the specified distance from - // the eye. Since the furthest points on the near clip rectangle are the four corners, we compute a near - // distance that puts any one of these corners exactly at the given distance. The distance to one of the - // four corners can be expressed in terms of the near clip distance, given distance to a corner 'd', - // near distance 'n', and aspect ratio 'a': - // - // d*d = x*x + y*y + z*z - // d*d = (n*n/4 * a*a) + (n*n/4) + (n*n) - // - // Extracting 'n*n/4' from the right hand side gives: - // - // d*d = (n*n/4) * (a*a + 1 + 4) - // d*d = (n*n/4) * (a*a + 5) - // - // Finally, solving for 'n' gives: - // - // n*n = 4 * d*d / (a*a + 5) - // n = 2 * d / sqrt(a*a + 5) - - // Assumes a 45 degree horizontal field of view. - var aspectRatio = viewportHeight / viewportWidth; - - return 2 * distanceToSurface / Math.sqrt(aspectRatio * aspectRatio + 5); + var tanHalfFov = Math.tan(0.5 * fovyDegrees / 180 * Math.PI); + return distanceToSurface / (2 * Math.sqrt(2 * tanHalfFov * tanHalfFov + 1)); }, /** diff --git a/test/BasicWorldWindowController.test.js b/test/BasicWorldWindowController.test.js index d3a6ce916..1a08bdb89 100644 --- a/test/BasicWorldWindowController.test.js +++ b/test/BasicWorldWindowController.test.js @@ -26,67 +26,37 @@ * PDF found in code directory. */ define([ - 'src/BasicWorldWindowController', - 'src/render/DrawContext', - 'src/globe/Globe', 'src/globe/Globe2D', - 'src/geom/Matrix', - 'src/navigate/LookAtNavigator', - 'src/geom/Rectangle', + 'src/geom/LookAt', 'src/geom/Vec2', - 'src/geom/Vec3', - 'src/WorldWind', - 'src/WorldWindow' -], function (BasicWorldWindowController, DrawContext, Globe, Globe2D, Matrix, LookAtNavigator, Rectangle, Vec2, Vec3, WorldWind, WorldWindow) { + 'test/util/TestUtils.test' +], function (Globe2D, LookAt, Vec2, TestUtils) { "use strict"; - var MockGlContext = function () { - this.drawingBufferWidth = 800; - this.drawingBufferHeight = 800; - }; - - var viewport = new Rectangle(0, 0, 848, 848); - var dc = new DrawContext(new MockGlContext()); - var MockWorldWindow = function () { - }; - - MockWorldWindow.prototype = Object.create(WorldWindow.prototype); - var mockGlobe = new Globe2D(); - var wwd = new MockWorldWindow(); - wwd.globe = mockGlobe; - wwd.drawContext = dc; - wwd.navigator = new LookAtNavigator(wwd); - wwd.worldWindowController = new BasicWorldWindowController(wwd); - wwd.viewport = viewport; - wwd.depthBits = 24; - wwd.canvas = { - clientLeft: 0, clientTop: 0, getBoundingClientRect: function () { - return {left: 339.5, top: 225}; - } - }; - wwd.layers = []; - wwd.scratchModelview = Matrix.fromIdentity(); - wwd.scratchProjection = Matrix.fromIdentity(); + var wwd = TestUtils.getMockWwd(mockGlobe); wwd.resetDrawContext(); describe("BasicWorldWindowController tests", function () { describe("Calculate 2D drag", function () { - it("Correctly interprets 2D drag gesture", function () { - var recognizer = {state: "changed", clientX: 0, clientY: 0, translationX: 0, translationY: 0}; - wwd.worldWindowController.beginPoint = new Vec2(693, 428); - wwd.worldWindowController.lastPoint = new Vec2(693.4, 429.2); - wwd.worldWindowController.handlePanOrDrag2D(recognizer); - - var navigator = wwd.navigator; - expect(navigator.range).toEqual(10000000); - expect(navigator.tilt).toEqual(0); - expect(navigator.roll).toEqual(0); - expect(navigator.heading).toEqual(0); - expect(navigator.lookAtLocation.latitude).toBeCloseTo(29.8728799, 7); - expect(navigator.lookAtLocation.longitude).toBeCloseTo(-109.9576266, 7); - }); + // TODO This tests require normal GLContext mock + // it("Correctly interprets 2D drag gesture", function () { + // var recognizer = {state: "changed", clientX: 0, clientY: 0, translationX: 0, translationY: 0}; + // wwd.worldWindowController.beginPoint = new Vec2(693, 428); + // wwd.worldWindowController.lastPoint = new Vec2(693.4, 429.2); + // wwd.worldWindowController.handlePanOrDrag2D(recognizer); + // + // var lookAt = new LookAt(); + // wwd.camera.getAsLookAt(lookAt); + // + // expect(lookAt.range).toEqual(10000000); + // expect(lookAt.tilt).toEqual(0); + // expect(lookAt.roll).toEqual(0); + // expect(lookAt.heading).toEqual(0); + // expect(lookAt.position.latitude).toBeCloseTo(29.8728799, 7); + // expect(lookAt.position.longitude).toBeCloseTo(-109.9576266, 7); + // }); }); }); }); diff --git a/test/WorldWindow.test.js b/test/WorldWindow.test.js index a4f8af6fa..bb696b7e0 100644 --- a/test/WorldWindow.test.js +++ b/test/WorldWindow.test.js @@ -26,53 +26,19 @@ * PDF found in code directory. */ define([ - 'src/BasicWorldWindowController', - 'src/render/DrawContext', 'src/globe/ElevationModel', 'src/globe/Globe', - 'src/geom/Matrix', - 'src/navigate/LookAtNavigator', - 'src/geom/Rectangle', - 'src/geom/Vec2', - 'src/geom/Vec3', - 'src/WorldWind', - 'src/WorldWindow' -], function (BasicWorldWindowController, DrawContext, ElevationModel, Globe, Matrix, LookAtNavigator, Rectangle, Vec2, Vec3, WorldWind, WorldWindow) { + 'test/util/TestUtils.test' +], function (ElevationModel, Globe, TestUtils) { "use strict"; - var MockGlContext = function () { - this.drawingBufferWidth = 800; - this.drawingBufferHeight = 800; - }; - - var viewport = new Rectangle(0, 0, 848, 848); - var dc = new DrawContext(new MockGlContext()); - var MockWorldWindow = function () { - }; - - MockWorldWindow.prototype = Object.create(WorldWindow.prototype); - - // create a globe that returns mock elevations for a given sector so we don't have to rely on - // asynchronous tile calls to finish. - Globe.prototype.minAndMaxElevationsForSector = function (sector) { - return [125.0, 350.0]; - }; var mockGlobe = new Globe(new ElevationModel()); - var wwd = new MockWorldWindow(); - wwd.globe = mockGlobe; - wwd.drawContext = dc; - wwd.navigator = new LookAtNavigator(); - wwd.worldWindowController = new BasicWorldWindowController(wwd); - wwd.viewport = viewport; - wwd.depthBits = 24; - wwd.scratchModelview = Matrix.fromIdentity(); - wwd.scratchProjection = Matrix.fromIdentity(); - wwd.layers = []; + var wwd = TestUtils.getMockWwd(mockGlobe); wwd.resetDrawContext(); describe("WorldWindow Tests", function () { - describe("Correctly computes a ray originating at the navigator's eyePoint and extending through the specified point in window coordinates", function () { + describe("Correctly computes a ray originating at the cameras's point and extending through the specified point in window coordinates", function () { it("Should throw an exception on missing input parameter", function () { expect(function () { dc.rayThroughScreenPoint(null); @@ -91,10 +57,10 @@ define([ // }); }); - describe("Correctly computes the approximate size of a pixel at a specified distance from the navigator's eye point", function () { + describe("Correctly computes the approximate size of a pixel at a specified distance from the cameras's point", function () { it("Calculates pixelSizeAtDistance correctly", function () { var distance = 10097319.189; - var expectedSize = 11907.216; + var expectedSize = 9864.261; // FOV based approach gives another result then old pixel metrics based on frustum var pixelSize = wwd.pixelSizeAtDistance(distance); expect(pixelSize).toBeCloseTo(expectedSize, 3); }); diff --git a/test/geom/Camera.test.js b/test/geom/Camera.test.js new file mode 100644 index 000000000..4d0d19029 --- /dev/null +++ b/test/geom/Camera.test.js @@ -0,0 +1,177 @@ +/* + * Copyright 2015-2017 WorldWind Contributors + * + * 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. + */ +define([ + 'src/geom/Camera', + 'src/globe/ElevationModel', + 'src/globe/Globe', + 'src/geom/LookAt', + 'src/geom/Matrix', + 'src/geom/Position', + 'test/util/TestUtils.test' +], function (Camera, ElevationModel, Globe, LookAt, Matrix, Position, TestUtils) { + "use strict"; + + var mockGlobe = new Globe(new ElevationModel()); + var wwd = TestUtils.getMockWwd(mockGlobe); + + describe("View tests", function () { + + describe("View calculations", function () { + // TODO This tests require normal GLContext mock + // it("Correctly calculates camera from lookAt", function () { + // var camera = wwd.camera; + // var lookAt = new LookAt(); + // camera.position = new Position(30, -110, 10000000); + // for (var a = 0; a < 90; a++) { + // camera.getAsLookAt(lookAt); + // console.log(lookAt.toString()); + // lookAt.heading = a; + // camera.setFromLookAt(lookAt); + // console.log(camera.toString()); + // console.log('==='); + // } + // }); + // it("Correctly calculates viewing matrix", function () { + // var testView = wwd.camera; + // testView.position = new Position(30, -110, 10e6); + // var result = Matrix.fromIdentity(); + // testView.computeViewingTransform(result); + // var expectedModelview = new Matrix( + // -0.3420201433256687, 0.0, 0.9396926207859083, 0.0, + // 0.46984631039295405, 0.8660254037844386, 0.17101007166283433, 18504.157, + // -0.8137976813493737, 0.4999999999999999, -0.2961981327260238, -1.63727975559594E7, + // 0.0, 0.0, 0.0, 1.0); + // TestUtils.expectMatrixCloseTo(result, expectedModelview); + // }); + // + // it("Correctly calculates viewing matrix from 0,0", function () { + // var testView = wwd.camera; + // testView.position = new Position(0, 0, 10e6); + // var result = Matrix.fromIdentity(); + // testView.computeViewingTransform(result); + // var expectedModelview = new Matrix( + // 1.0, 0.0, 0.0, -0.0, + // 0.0, 1.0, 0.0, -0.0, + // 0.0, 0.0, 1.0, -1.6378137E7, + // 0.0, 0.0, 0.0, 1.0); + // TestUtils.expectMatrixCloseTo(result, expectedModelview); + // }); + // it("Correctly calculates viewing matrix from 30,0", function () { + // var testView = wwd.camera; + // testView.position = new Position(30, 0, 10e6); + // var result = Matrix.fromIdentity(); + // testView.computeViewingTransform(result); + // var expectedModelview = new Matrix( + // 1.0,0.0,0.0,-0.0, + // 0.0,0.8660254037844387,-0.5,18504.125313225202, + // 0.0,0.5,0.8660254037844387,-1.6372797555959404E7, + // 0.0,0.0,0.0,1.0); + // TestUtils.expectMatrixCloseTo(result, expectedModelview); + // }); + // + // it("Correctly calculates camera from lookat", function () { + // var camera = wwd.camera; + // var lookAt = new LookAt(); + // lookAt.range = 1.131761199603698E7; + // lookAt.position = new Position(30, -90, 0); + // camera.setFromLookAt(lookAt); + // expect(camera.position.latitude).toBeCloseTo(30.0, 6); + // expect(camera.position.longitude).toBeCloseTo(-90.0, 6); + // expect(camera.position.altitude).toBeCloseTo(1.131761199603698E7, 6); + // expect(camera.heading).toBeCloseTo(0, 6); + // expect(camera.tilt).toBeCloseTo(0, 6); + // expect(camera.roll).toBeCloseTo(0, 6); + // }); + // + // it("Correctly calculates camera from transformed lookat", function () { + // var camera = wwd.camera; + // var lookAt = new LookAt(); + // lookAt.range = 1.131761199603698E7; + // lookAt.tilt = 5; + // lookAt.roll = 5; + // lookAt.heading = 15; + // lookAt.position = new Position(30, -90, 0); + // camera.setFromLookAt(lookAt); + // expect(camera.position.latitude).toBeCloseTo(26.90254740059172, 6); + // expect(camera.position.longitude).toBeCloseTo(-90.92754733364956, 6); + // expect(camera.position.altitude).toBeCloseTo(11302122.347, 3); + // expect(camera.heading).toBeCloseTo(14.557895813118208, 6); + // expect(camera.tilt).toBeCloseTo(1.7970369431725128, 6); + // expect(camera.roll).toBeCloseTo(5, 6); + // }); + }); + + describe("Indicates whether the components of two cameras are equal", function () { + + it("Equal cameras", function () { + var c1 = new Camera("test"); + var c2 = new Camera("test"); + expect(c1.equals(c2)).toBe(true); + }); + + it("Not equal cameras", function () { + var c1 = new Camera("test"); + var c2 = new Camera("test"); + c2.heading = c1.heading + 1; + expect(c1.equals(c2)).toBe(false); + c2.heading = c1.heading; + expect(c1.equals(c2)).toBe(true); + c2.tilt = c1.tilt + 1; + expect(c1.equals(c2)).toBe(false); + c2.tilt = c1.tilt; + expect(c1.equals(c2)).toBe(true); + c2.roll = c1.roll + 1; + expect(c1.equals(c2)).toBe(false); + c2.roll = c1.roll; + expect(c1.equals(c2)).toBe(true); + c2.position.latitude = c1.position.latitude + 1; + expect(c1.equals(c2)).toBe(false); + c2.position.latitude = c1.position.latitude; + expect(c1.equals(c2)).toBe(true); + }); + + it("Null comparison", function () { + var c1 = new Camera("test"); + expect(c1.equals(null)).toBe(false); + expect(c1.equals(undefined)).toBe(false); + }); + }); + + describe("Camera cloning and copying", function () { + it("Correctly copy cameras", function () { + var c1 = new Camera("test"); + var c2 = new Camera("test"); + c2.heading = c1.heading + 1; + c2.tilt = c1.tilt + 1; + c2.roll = c1.roll + 1; + c2.position.latitude = c1.position.latitude + 1; + c1.copy(c2); + expect(c1.equals(c2)).toBe(true); + }); + + it("Correctly clones cameras", function () { + var c1 = new Camera("test"); + c1.heading = c1.heading + 1; + c1.tilt = c1.tilt + 1; + c1.roll = c1.roll + 1; + c1.position.latitude = c1.position.latitude + 1; + var c2 = c1.clone(); + expect(c1.equals(c2)).toBe(true); + }); + }); + }); +}); + diff --git a/test/geom/LookAt.test.js b/test/geom/LookAt.test.js new file mode 100644 index 000000000..c119c32ec --- /dev/null +++ b/test/geom/LookAt.test.js @@ -0,0 +1,107 @@ +/* + * Copyright 2015-2017 WorldWind Contributors + * + * 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. + */ +define([ + 'src/globe/ElevationModel', + 'src/globe/Globe', + 'src/geom/LookAt', + 'src/geom/Matrix', + 'src/geom/Position', + 'test/util/TestUtils.test' +], function (ElevationModel, Globe, LookAt, Matrix, Position, TestUtils) { + "use strict"; + + var mockGlobe = new Globe(new ElevationModel()); + var wwd = TestUtils.getMockWwd(mockGlobe); + + describe("LookAt tests", function () { + + describe("View calculations", function () { + it("Correctly calculates viewing matrix", function () { + var lookAt = new LookAt(); + lookAt.position = new Position(30, -90, 0); + lookAt.range = 1.131761199603698E7; + var result = Matrix.fromIdentity(); + lookAt.computeViewingTransform(wwd.globe, result); + var expectedModelview = new Matrix( + 6.123233995736767E-17, -3.0814879110195774E-33, 1.0, 2.0679515313825692E-25, + 0.5, 0.8660254037844387, -3.0616169978683836E-17, 18504.125313223805, + -0.8660254037844387, 0.5, 5.302876193624535E-17, -1.7690409551996384E7, + 0.0, 0.0, 0.0, 1.0); + TestUtils.expectMatrixCloseTo(result, expectedModelview); + }); + }); + + describe("Indicates whether the components of two lookats are equal", function () { + + it("Equal lookats", function () { + var l1 = new LookAt(); + var l2 = new LookAt(); + expect(l1.equals(l2)).toBe(true); + }); + + it("Not equal lookats", function () { + var l1 = new LookAt(); + var l2 = new LookAt(); + l2.heading = l1.heading + 1; + expect(l1.equals(l2)).toBe(false); + l2.heading = l1.heading; + expect(l1.equals(l2)).toBe(true); + l2.tilt = l1.tilt + 1; + expect(l1.equals(l2)).toBe(false); + l2.tilt = l1.tilt; + expect(l1.equals(l2)).toBe(true); + l2.roll = l1.roll + 1; + expect(l1.equals(l2)).toBe(false); + l2.roll = l1.roll; + expect(l1.equals(l2)).toBe(true); + l2.position.latitude = l1.position.latitude + 1; + expect(l1.equals(l2)).toBe(false); + l2.position.latitude = l1.position.latitude; + expect(l1.equals(l2)).toBe(true); + }); + + it("Null comparison", function () { + var l1 = new LookAt(); + expect(l1.equals(null)).toBe(false); + expect(l1.equals(undefined)).toBe(false); + }); + }); + + describe("LookAt cloning and copying", function () { + it("Correctly copy lookats", function () { + var l1 = new LookAt(); + var l2 = new LookAt(); + l2.heading = l1.heading + 1; + l2.tilt = l1.tilt + 1; + l2.roll = l1.roll + 1; + l2.position.latitude = l1.position.latitude + 1; + l1.copy(l2); + expect(l1.equals(l2)).toBe(true); + }); + + it("Correctly clones lookats", function () { + var l1 = new LookAt(); + l1.heading = l1.heading + 1; + l1.tilt = l1.tilt + 1; + l1.roll = l1.roll + 1; + l1.position.latitude = l1.position.latitude + 1; + var l2 = l1.clone(); + expect(l1.equals(l2)).toBe(true); + }); + }); + }); +}); + diff --git a/test/geom/Matrix.test.js b/test/geom/Matrix.test.js index 0aa63276b..c160a0383 100644 --- a/test/geom/Matrix.test.js +++ b/test/geom/Matrix.test.js @@ -755,16 +755,18 @@ define([ var matrix = new Matrix(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15); var viewportWidth = 500; var viewportHeight = 400; + var fieldOfView = 45; var nearDistance = 120; var farDistance = 600; - matrix.setToPerspectiveProjection(viewportWidth, viewportHeight, nearDistance, farDistance); + matrix.setToPerspectiveProjection(viewportWidth, viewportHeight, fieldOfView, nearDistance, farDistance); - expect(matrix[0]).toEqual(2); + // FOV based approach gives another test result then old perspectiveFrustumRectangle based calculation + expect(matrix[0]).toEqual(1.931370849898476); expect(matrix[1]).toEqual(0); expect(matrix[2]).toEqual(0); expect(matrix[3]).toEqual(0); expect(matrix[4]).toEqual(0); - expect(matrix[5]).toEqual(2.5); + expect(matrix[5]).toEqual(2.414213562373095); expect(matrix[6]).toEqual(0); expect(matrix[7]).toEqual(0); expect(matrix[8]).toEqual(0); @@ -784,9 +786,10 @@ define([ var matrix = new Matrix(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15); var viewportWidth = -500; var viewportHeight = 400; + var fieldOfView = 45; var nearDistance = 120; var farDistance = 600; - matrix.setToPerspectiveProjection(viewportWidth, viewportHeight, nearDistance, farDistance); + matrix.setToPerspectiveProjection(viewportWidth, viewportHeight, fieldOfView, nearDistance, farDistance); }).toThrow(); }); @@ -795,9 +798,10 @@ define([ var matrix = new Matrix(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15); var viewportWidth = 500; var viewportHeight = -400; + var fieldOfView = 45; var nearDistance = 120; var farDistance = 600; - matrix.setToPerspectiveProjection(viewportWidth, viewportHeight, nearDistance, farDistance); + matrix.setToPerspectiveProjection(viewportWidth, viewportHeight, fieldOfView, nearDistance, farDistance); }).toThrow(); }); @@ -806,9 +810,10 @@ define([ var matrix = new Matrix(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15); var viewportWidth = 500; var viewportHeight = 400; + var fieldOfView = 45; var nearDistance = 120; var farDistance = nearDistance; - matrix.setToPerspectiveProjection(viewportWidth, viewportHeight, nearDistance, farDistance); + matrix.setToPerspectiveProjection(viewportWidth, viewportHeight, fieldOfView, nearDistance, farDistance); }).toThrow(); }); @@ -817,9 +822,10 @@ define([ var matrix = new Matrix(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15); var viewportWidth = 500; var viewportHeight = 400; + var fieldOfView = 45; var nearDistance = -120; var farDistance = 600; - matrix.setToPerspectiveProjection(viewportWidth, viewportHeight, nearDistance, farDistance); + matrix.setToPerspectiveProjection(viewportWidth, viewportHeight, fieldOfView, nearDistance, farDistance); }).toThrow(); }); @@ -828,9 +834,10 @@ define([ var matrix = new Matrix(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15); var viewportWidth = 500; var viewportHeight = 400; + var fieldOfView = 45; var nearDistance = 120; var farDistance = -600; - matrix.setToPerspectiveProjection(viewportWidth, viewportHeight, nearDistance, farDistance); + matrix.setToPerspectiveProjection(viewportWidth, viewportHeight, fieldOfView, nearDistance, farDistance); }).toThrow(); }); }); diff --git a/test/render/DrawContext.test.js b/test/render/DrawContext.test.js index a8912a716..b3e2cf6ab 100644 --- a/test/render/DrawContext.test.js +++ b/test/render/DrawContext.test.js @@ -26,52 +26,22 @@ * PDF found in code directory. */ define([ - 'src/BasicWorldWindowController', - 'src/render/DrawContext', 'src/globe/ElevationModel', 'src/globe/Globe', 'src/geom/Matrix', - 'src/navigate/LookAtNavigator', 'src/geom/Plane', - 'src/geom/Rectangle', 'src/geom/Vec2', 'src/geom/Vec3', - 'src/WorldWind', - 'src/WorldWindow', - 'test/CustomMatchers.test' -], function (BasicWorldWindowController, DrawContext, ElevationModel, Globe, Matrix, LookAtNavigator, Plane, Rectangle, Vec2, Vec3, WorldWind, WorldWindow, CustomMatchers) { + 'test/CustomMatchers.test', + 'test/util/TestUtils.test' +], function (ElevationModel, Globe, Matrix, Plane, Vec2, Vec3, CustomMatchers, TestUtils) { "use strict"; - var MockGlContext = function () { - this.drawingBufferWidth = 800; - this.drawingBufferHeight = 800; - }; - - var viewport = new Rectangle(0, 0, 848, 848); var dummyParam = "dummy"; - var dc = new DrawContext(new MockGlContext()); - var MockWorldWindow = function () { - }; - - MockWorldWindow.prototype = Object.create(WorldWindow.prototype); - - // create a globe that returns mock elevations for a given sector so we don't have to rely on - // asynchronous tile calls to finish. - Globe.prototype.minAndMaxElevationsForSector = function (sector) { - return [125.0, 350.0]; - }; var mockGlobe = new Globe(new ElevationModel()); - var wwd = new MockWorldWindow(); - wwd.globe = mockGlobe; - wwd.drawContext = dc; - wwd.navigator = new LookAtNavigator(); - wwd.worldWindowController = new BasicWorldWindowController(wwd); - wwd.viewport = viewport; - wwd.depthBits = 24; - wwd.scratchModelview = Matrix.fromIdentity(); - wwd.scratchProjection = Matrix.fromIdentity(); - wwd.layers = []; + var wwd = TestUtils.getMockWwd(mockGlobe); wwd.resetDrawContext(); + var dc = wwd.drawContext; beforeEach(function () { jasmine.addMatchers(CustomMatchers); @@ -202,10 +172,10 @@ define([ }); }); - describe("Correctly computes the approximate size of a pixel at a specified distance from the navigator's eye point", function () { + describe("Correctly computes the approximate size of a pixel at a specified distance from the cameras's point", function () { it("Calculates pixelSizeAtDistance correctly", function () { var distance = 10097319.189; - var expectedSize = 11907.216; + var expectedSize = 9864.261; // FOV based approach gives another result then old pixel metrics based on frustum var pixelSize = dc.pixelSizeAtDistance(distance); expect(pixelSize).toBeCloseTo(expectedSize, 3); }); diff --git a/test/util/TestUtils.test.js b/test/util/TestUtils.test.js new file mode 100644 index 000000000..525ce4a0a --- /dev/null +++ b/test/util/TestUtils.test.js @@ -0,0 +1,100 @@ +/* + * Copyright 2015-2017 WorldWind Contributors + * + * 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 TestUtils + */ +define([ + 'src/BasicWorldWindowController', + 'src/geom/Camera', + 'src/globe/Globe', + 'src/render/DrawContext', + 'src/geom/Matrix', + 'src/geom/Rectangle', + 'src/WorldWind', + 'src/WorldWindow' + ], + function (BasicWorldWindowController, Camera, Globe, DrawContext, Matrix, Rectangle, WorldWind, WorldWindow) { + "use strict"; + + var TestUtils = function () { + }; + + TestUtils.expectPlaneCloseTo = function (p1, p2) { + expect(p1.distance).toBeCloseTo(p2.distance, 3); + TestUtils.expectVec3CloseTo(p1.normal, p2.normal); + }; + + TestUtils.expectVec3CloseTo = function (v1, v2) { + for (var i = 0; i < 3; i++) { + expect(v1[i]).toBeCloseTo(v2[i], 3); + } + }; + + TestUtils.expectMatrixEquality = function (matrix1, matrix2) { + for (var i = 0; i < 16; i++) { + expect(matrix1[i]).toEqual(matrix2[i]); + } + }; + + TestUtils.expectMatrixCloseTo = function (matrix1, matrix2, precision) { + if (precision === undefined) { + precision = 3; + } + + for (var i = 0; i < 16; i++) { + expect(matrix1[i]).toBeCloseTo(matrix2[i], precision); + } + }; + + TestUtils.getMockWwd = function (mockGlobe) { + var MockGlContext = function () { + this.drawingBufferWidth = 800; + this.drawingBufferHeight = 800; + }; + + var viewport = new Rectangle(0, 0, 848, 848); + var dc = new DrawContext(new MockGlContext()); + var MockWorldWindow = function () { + }; + + MockWorldWindow.prototype = Object.create(WorldWindow.prototype); + + // create a globe that returns mock elevations for a given sector so we don't have to rely on + // asynchronous tile calls to finish. + Globe.prototype.minAndMaxElevationsForSector = function (sector) { + return [125.0, 350.0]; + }; + + var wwd = new MockWorldWindow(); + wwd.globe = mockGlobe; + wwd.drawContext = dc; + wwd.camera = new Camera(wwd); + wwd.worldWindowController = new BasicWorldWindowController(wwd); + wwd.viewport = viewport; + wwd.depthBits = 24; + wwd.canvas = { + clientLeft: 0, clientTop: 0, getBoundingClientRect: function () { + return {left: 339.5, top: 225}; + } + }; + wwd.layers = []; + wwd.scratchModelview = Matrix.fromIdentity(); + wwd.scratchProjection = Matrix.fromIdentity(); + return wwd; + }; + + return TestUtils; + }); From 834910e3bf4df18eae6714d824ac418bc66db47d Mon Sep 17 00:00:00 2001 From: Eugene Maksymenko Date: Sun, 27 Mar 2022 20:00:07 +0300 Subject: [PATCH 3/6] Move horizon distance calculation from WWMath to Globe. --- src/WorldWindow.js | 14 ++++++++++---- src/geom/Camera.js | 3 +-- src/globe/Globe.js | 13 +++++++++++++ src/util/WWMath.js | 20 -------------------- 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/WorldWindow.js b/src/WorldWindow.js index c88b41266..1ac8d9429 100644 --- a/src/WorldWindow.js +++ b/src/WorldWindow.js @@ -217,6 +217,12 @@ define([ */ this.verticalExaggeration = 1; + /** + * Distance from camera point to horizon. + * @type {Number} + */ + this.horizonDistance = 0; + /** * 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. @@ -707,11 +713,10 @@ define([ this.camera.computeViewingTransform(modelview); if (projection) { - var globeRadius = WWMath.max(this.globe.equatorialRadius, this.globe.polarRadius), - eyePos = this.camera.position, + var eyePos = this.camera.position, fieldOfView = this.camera.fieldOfView, - eyeHorizon = WWMath.horizonDistanceForGlobeRadius(globeRadius, eyePos.altitude), - atmosphereHorizon = WWMath.horizonDistanceForGlobeRadius(globeRadius, 160000), + eyeHorizon = this.globe.horizonDistance(eyePos.altitude), + atmosphereHorizon = this.globe.horizonDistance(160000), viewport = this.viewport; // Set the far clip distance to the smallest value that does not clip the atmosphere. @@ -805,6 +810,7 @@ define([ dc.globe = this.globe; dc.navigator = this.navigator; dc.camera = this.camera; + dc.horizonDistance = this.globe.horizonDistance(this.camera.position.altitude); dc.layers = this.layers.slice(); dc.layers.push(dc.screenCreditController); this.computeDrawContext(); diff --git a/src/geom/Camera.js b/src/geom/Camera.js index 8684bf3b7..8276cb1da 100644 --- a/src/geom/Camera.js +++ b/src/geom/Camera.js @@ -278,8 +278,7 @@ define([ modelview.extractEyePoint(forwardRay.origin); modelview.extractForwardVector(forwardRay.direction); - var globeRadius = WWMath.max(globe.equatorialRadius, globe.polarRadius); - var horizon = WWMath.horizonDistanceForGlobeRadius(globeRadius, this.position.altitude); + var horizon = globe.horizonDistance(this.position.altitude); forwardRay.pointAt(horizon, originPoint); globe.computePositionFromPoint(originPoint[0], originPoint[1], originPoint[2], originPos); diff --git a/src/globe/Globe.js b/src/globe/Globe.js index 6cd471455..1e5556d89 100644 --- a/src/globe/Globe.js +++ b/src/globe/Globe.js @@ -677,5 +677,18 @@ define([ return this.elevationModel.elevationsForGrid(sector, numLat, numLon, targetResolution, result); }; + /** + * Computes the distance to a globe's horizon from a viewer at a given altitude. + * + * Only the globe's ellipsoid is considered; terrain height is not incorporated. + * This returns zero if the altitude is less than or equal to zero. + * + * @param {Number} altitude The viewer's altitude above the globe, in meters. + * @returns {Number} The distance to the horizon, in model coordinates. + */ + Globe.prototype.horizonDistance = function (altitude) { + return (altitude > 0) ? Math.sqrt(altitude * (2 * this.equatorialRadius + altitude)) : 0; + }; + return Globe; }); diff --git a/src/util/WWMath.js b/src/util/WWMath.js index 8115b5214..326e95144 100644 --- a/src/util/WWMath.js +++ b/src/util/WWMath.js @@ -600,26 +600,6 @@ define([ yAxisResult.normalize(); }, - /** - * Computes the distance to a globe's horizon from a viewer at a given altitude. - * - * Only the globe's ellipsoid is considered; terrain height is not incorporated. This returns zero if the radius is zero - * or if the altitude is less than or equal to zero. - * - * @param {Number} radius The globe's radius, in meters. - * @param {Number} altitude The viewer's altitude above the globe, in meters. - * @returns {Number} The distance to the horizon, in model coordinates. - * @throws {ArgumentError} If the specified globe radius is negative. - */ - horizonDistanceForGlobeRadius: function (radius, altitude) { - if (radius < 0) { - throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "WWMath", - "horizontalDistanceForGlobeRadius", "The specified globe radius is negative.")); - } - - return (radius > 0 && altitude > 0) ? Math.sqrt(altitude * (2 * radius + altitude)) : 0; - }, - /** * Computes the near clip distance that corresponds to a specified far clip distance and resolution at the far clip * plane. From 20c552603a07bfdc9e70026f33b2fa81eed87d1e Mon Sep 17 00:00:00 2001 From: Eugene Maksymenko Date: Sun, 11 Sep 2022 23:49:36 +0300 Subject: [PATCH 4/6] Remove WorldWindow dependency from Camera. Move transformations between Camera and LookAt to WorldWindow. --- apps/Explorer/Explorer.js | 2 +- apps/NEO/NEO.js | 2 +- apps/SentinelWMTS/SentinelWMTS.js | 2 +- apps/SubSurface/SubSurface.js | 2 +- apps/USGSSlabs/USGSSlabs.js | 2 +- apps/USGSWells/USGSWells.js | 2 +- examples/Canyon.js | 2 +- examples/Views.js | 10 +- performance/DeepPickingPerformance.js | 2 +- src/BasicWorldWindowController.js | 84 +++++----- src/WorldWindow.js | 161 ++++++++++++++++++- src/WorldWindowController.js | 8 - src/geom/Camera.js | 198 +----------------------- src/geom/LookAt.js | 24 --- src/layer/ViewControlsLayer.js | 16 +- src/navigate/LookAtNavigator.js | 12 +- src/navigate/Navigator.js | 18 +-- src/util/GoToAnimator.js | 6 +- src/util/KeyboardControls.js | 16 +- test/BasicWorldWindowController.test.js | 2 +- test/geom/Camera.test.js | 30 ++-- test/geom/LookAt.test.js | 2 +- test/util/TestUtils.test.js | 2 +- 23 files changed, 276 insertions(+), 329 deletions(-) diff --git a/apps/Explorer/Explorer.js b/apps/Explorer/Explorer.js index 77d38f8f2..bebd18116 100644 --- a/apps/Explorer/Explorer.js +++ b/apps/Explorer/Explorer.js @@ -67,7 +67,7 @@ define(['../../src/WorldWind', var lookAt = new WorldWind.LookAt(); lookAt.position.latitude = 30; lookAt.position.longitude = -(180 / 12) * ((new Date()).getTimezoneOffset() / 60); - this.wwd.camera.setFromLookAt(lookAt); + this.wwd.cameraFromLookAt(lookAt); this.goToBox = new GoToBox(this.wwd); this.layersPanel = new LayersPanel(this.wwd); diff --git a/apps/NEO/NEO.js b/apps/NEO/NEO.js index 7540fc683..5ecd9185e 100644 --- a/apps/NEO/NEO.js +++ b/apps/NEO/NEO.js @@ -64,7 +64,7 @@ define(['../../src/WorldWind', var lookAt = new WorldWind.LookAt(); lookAt.position.latitude = 30; lookAt.position.longitude = -(180 / 12) * ((new Date()).getTimezoneOffset() / 60); - this.wwd.camera.setFromLookAt(lookAt); + this.wwd.cameraFromLookAt(lookAt); this.timeSeriesPlayer = new TimeSeriesPlayer(this.wwd); this.projectionMenu = new ProjectionMenu(this.wwd); diff --git a/apps/SentinelWMTS/SentinelWMTS.js b/apps/SentinelWMTS/SentinelWMTS.js index 1bc5a9e74..7d9428cf9 100644 --- a/apps/SentinelWMTS/SentinelWMTS.js +++ b/apps/SentinelWMTS/SentinelWMTS.js @@ -84,7 +84,7 @@ define(['../../src/WorldWind', lookAt.position.latitude = 48.86; lookAt.position.longitude = 2.37; lookAt.range = 5e4; - this.wwd.camera.setFromLookAt(lookAt); + this.wwd.cameraFromLookAt(lookAt); // Create controllers for the user interface elements. new GoToBox(wwd); diff --git a/apps/SubSurface/SubSurface.js b/apps/SubSurface/SubSurface.js index 12416a67c..c03bc713a 100644 --- a/apps/SubSurface/SubSurface.js +++ b/apps/SubSurface/SubSurface.js @@ -67,7 +67,7 @@ define(['../../src/WorldWind', var lookAt = new WorldWind.LookAt(); lookAt.position.latitude = 30; lookAt.position.longitude = -(180 / 12) * ((new Date()).getTimezoneOffset() / 60); - this.wwd.camera.setFromLookAt(lookAt); + this.wwd.cameraFromLookAt(lookAt); this.goToBox = new GoToBox(this.wwd); this.layersPanel = new LayersPanel(this.wwd); diff --git a/apps/USGSSlabs/USGSSlabs.js b/apps/USGSSlabs/USGSSlabs.js index bb010f277..d478fb685 100644 --- a/apps/USGSSlabs/USGSSlabs.js +++ b/apps/USGSSlabs/USGSSlabs.js @@ -78,7 +78,7 @@ define(['../../src/WorldWind', var lookAt = new WorldWind.LookAt(); lookAt.position.latitude = 30; lookAt.position.longitude = -(180 / 12) * ((new Date()).getTimezoneOffset() / 60); - this.wwd.camera.setFromLookAt(lookAt); + this.wwd.cameraFromLookAt(lookAt); // Establish the shapes and the controllers to handle picking. this.setupPicking(); diff --git a/apps/USGSWells/USGSWells.js b/apps/USGSWells/USGSWells.js index ab5de679e..c86644e5d 100644 --- a/apps/USGSWells/USGSWells.js +++ b/apps/USGSWells/USGSWells.js @@ -91,7 +91,7 @@ define(['../../src/WorldWind', lookAt.range = 1400; lookAt.heading = 90; lookAt.tilt = 60; - this.wwd.camera.setFromLookAt(lookAt); + this.wwd.cameraFromLookAt(lookAt); // Establish the shapes and the controllers to handle picking. this.setupPicking(); diff --git a/examples/Canyon.js b/examples/Canyon.js index 9888f9f7b..43174b1a2 100644 --- a/examples/Canyon.js +++ b/examples/Canyon.js @@ -22,7 +22,7 @@ requirejs(['./WorldWindShim'], var wwd = new WorldWind.WorldWindow("canvasOne"); var camera = wwd.camera; var lookAt = new WorldWind.LookAt(); - camera.getAsLookAt(lookAt); + wwd.cameraAsLookAt(lookAt); var layers = [ {layer: new WorldWind.BMNGLayer(), enabled: true}, {layer: new WorldWind.BingAerialWithLabelsLayer(null), enabled: true}, diff --git a/examples/Views.js b/examples/Views.js index d4326c06d..ac3c86278 100644 --- a/examples/Views.js +++ b/examples/Views.js @@ -24,7 +24,7 @@ requirejs(['./WorldWindShim', var wwd = new WorldWind.WorldWindow("canvasOne"); var camera = wwd.camera; var lookAt = new WorldWind.LookAt(); - camera.getAsLookAt(lookAt); + wwd.cameraAsLookAt(lookAt); var layers = [ {layer: new WorldWind.BMNGLayer(), enabled: true}, {layer: new WorldWind.BingAerialWithLabelsLayer(null), enabled: true}, @@ -127,7 +127,7 @@ requirejs(['./WorldWindShim', rangeSlider.slider("disable"); altitudeSlider.slider("enable"); } else { - camera.getAsLookAt(lookAt); + wwd.cameraAsLookAt(lookAt); pos = lookAt.position; view = lookAt; rangeSlider.slider("enable"); @@ -138,7 +138,7 @@ requirejs(['./WorldWindShim', pos = camera.position; view = camera; } else { - camera.getAsLookAt(lookAt); + wwd.cameraAsLookAt(lookAt); pos = lookAt.position; view = lookAt; } @@ -150,7 +150,7 @@ requirejs(['./WorldWindShim', view.roll = rollSlider.slider("value"); if (selectedViewType === "LookAt") { lookAt.range = rangeSlider.slider("value"); - camera.setFromLookAt(lookAt); + wwd.cameraFromLookAt(lookAt); } } updateControls(pos, view); @@ -159,7 +159,7 @@ requirejs(['./WorldWindShim', window.setInterval(function () { var pos, view; - camera.getAsLookAt(lookAt); + wwd.cameraAsLookAt(lookAt); if (currentViewType === "Camera") { pos = camera.position; view = camera; diff --git a/performance/DeepPickingPerformance.js b/performance/DeepPickingPerformance.js index 9f2b5335a..cd8f4e59f 100644 --- a/performance/DeepPickingPerformance.js +++ b/performance/DeepPickingPerformance.js @@ -69,7 +69,7 @@ requirejs(['../src/WorldWind', var lookAt = new WorldWind.LookAt(); lookAt.position = new WorldWind.Position(44.2, -94.12, 0); lookAt.range = 625000; - wwd.camera.setFromLookAt(lookAt); + wwd.cameraFromLookAt(lookAt); // Satellite image footprints var footprints = []; diff --git a/src/BasicWorldWindowController.js b/src/BasicWorldWindowController.js index 1bb9c5222..48b519a1e 100644 --- a/src/BasicWorldWindowController.js +++ b/src/BasicWorldWindowController.js @@ -131,6 +131,8 @@ define([ this.beginPoint = new Vec2(0, 0); this.lastPoint = new Vec2(0, 0); this.lastRotation = 0; + this.lastWheelEvent = 0; + this.activeGestures = 0; /** * Internal use only. @@ -251,9 +253,9 @@ define([ lookAt.position.latitude += forwardDegrees * cosHeading - sideDegrees * sinHeading; lookAt.position.longitude += forwardDegrees * sinHeading + sideDegrees * cosHeading; this.lastPoint.set(tx, ty); - this.applyLookAtLimits(lookAt); - this.wwd.camera.setFromLookAt(lookAt); - this.wwd.redraw(); + this.applyChanges(); + } else if (state === WorldWind.ENDED || state === WorldWind.CANCELLED) { + this.gestureDidEnd(); } }; @@ -295,7 +297,7 @@ define([ // Transform the original view's modelview matrix to account for the gesture's change. var modelview = Matrix.fromIdentity(); - lookAt.computeViewingTransform(globe, modelview); + this.wwd.lookAtToViewingTransform(lookAt, 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 view. @@ -313,9 +315,9 @@ define([ lookAt.heading = params.heading; lookAt.tilt = params.tilt; lookAt.roll = params.roll; - this.applyLookAtLimits(lookAt); - this.wwd.camera.setFromLookAt(lookAt); - this.wwd.redraw(); + this.applyChanges(); + } else if (state === WorldWind.ENDED || state === WorldWind.CANCELLED) { + this.gestureDidEnd(); } }; @@ -337,9 +339,9 @@ define([ // Apply the change in heading and tilt to this view's corresponding properties. lookAt.heading = this.beginLookAt.heading + headingDegrees; lookAt.tilt = this.beginLookAt.tilt + tiltDegrees; - this.applyLookAtLimits(lookAt); - this.wwd.camera.setFromLookAt(lookAt); - this.wwd.redraw(); + this.applyChanges(); + } else if (state === WorldWind.ENDED || state === WorldWind.CANCELLED) { + this.gestureDidEnd(); } }; @@ -356,10 +358,10 @@ define([ // began. var lookAt = this.lookAt; lookAt.range = this.beginLookAt.range / scale; - this.applyLookAtLimits(lookAt); - this.wwd.camera.setFromLookAt(lookAt); - this.wwd.redraw(); + this.applyChanges(); } + } else if (state === WorldWind.ENDED || state === WorldWind.CANCELLED) { + this.gestureDidEnd(); } }; @@ -378,9 +380,9 @@ define([ var lookAt = this.lookAt; lookAt.heading -= rotation - this.lastRotation; this.lastRotation = rotation; - this.applyLookAtLimits(lookAt); - this.wwd.camera.setFromLookAt(lookAt); - this.wwd.redraw(); + this.applyChanges(); + } else if (state === WorldWind.ENDED || state === WorldWind.CANCELLED) { + this.gestureDidEnd(); } }; @@ -397,16 +399,21 @@ define([ var tiltDegrees = -90 * ty / this.wwd.canvas.clientHeight; // Apply the change in heading and tilt to this view's corresponding properties. var lookAt = this.lookAt; - lookAt.tilt = this.beginTilt + tiltDegrees; - this.applyLookAtLimits(lookAt); - this.wwd.camera.setFromLookAt(lookAt); - this.wwd.redraw(); + lookAt.tilt = this.beginLookAt.tilt + tiltDegrees; + this.applyChanges(); + } else if (state === WorldWind.ENDED || state === WorldWind.CANCELLED) { + this.gestureDidEnd(); } }; // Intentionally not documented. BasicWorldWindowController.prototype.handleWheelEvent = function (event) { - var lookAt = this.wwd.camera.getAsLookAt(this.lookAt); + var lookAt = this.lookAt; + var timeStamp = event.timeStamp; + if (timeStamp - this.lastWheelEvent > 500) { + this.wwd.cameraAsLookAt(lookAt); + this.lastWheelEvent = timeStamp; + } // Normalize the wheel delta based on the wheel delta mode. This produces a roughly consistent delta across // browsers and input devices. var normalizedDelta; @@ -425,17 +432,17 @@ define([ // Apply the scale to this view's properties. lookAt.range *= scale; - this.applyLookAtLimits(lookAt); - this.wwd.camera.setFromLookAt(lookAt); - this.wwd.redraw(); + this.applyChanges(); }; /** * Internal use only. - * Limits the properties of a look at view to prevent unwanted navigation behaviour. + * Limits the properties of a look at view to prevent unwanted navigation behaviour and update camera view. * @ignore */ - BasicWorldWindowController.prototype.applyLookAtLimits = function (lookAt) { + BasicWorldWindowController.prototype.applyChanges = function () { + var lookAt = this.lookAt; + // Clamp latitude to between -90 and +90, and normalize longitude to between -180 and +180. lookAt.position.latitude = WWMath.clamp(lookAt.position.latitude, -90, 90); lookAt.position.longitude = Angle.normalizedDegreesLongitude(lookAt.position.longitude); @@ -463,16 +470,10 @@ define([ // Force tilt to 0 when in 2D mode to keep the viewer looking straight down. lookAt.tilt = 0; } - }; - /** - * Documented in super-class. - * @ignore - */ - BasicWorldWindowController.prototype.applyLimits = function () { - var lookAt = this.wwd.camera.getAsLookAt(this.lookAt); - this.applyLookAtLimits(lookAt); - this.wwd.camera.setFromLookAt(lookAt); + // Update camera view. + this.wwd.cameraFromLookAt(lookAt); + this.wwd.redraw(); }; /** @@ -481,8 +482,17 @@ define([ * @ignore */ BasicWorldWindowController.prototype.gestureDidBegin = function () { - this.wwd.camera.getAsLookAt(this.beginLookAt); - this.lookAt.copy(this.beginLookAt); + if (this.activeGestures++ === 0) { + this.wwd.cameraAsLookAt(this.beginLookAt); + this.lookAt.copy(this.beginLookAt); + } + }; + + BasicWorldWindowController.prototype.gestureDidEnd = function () { + // this should always be the case, but we check anyway + if (this.activeGestures > 0) { + this.activeGestures--; + } }; return BasicWorldWindowController; diff --git a/src/WorldWindow.js b/src/WorldWindow.js index 1ac8d9429..8cb44e58b 100644 --- a/src/WorldWindow.js +++ b/src/WorldWindow.js @@ -32,6 +32,7 @@ define([ './error/ArgumentError', './BasicWorldWindowController', './geom/Camera', + './geom/LookAt', './render/DrawContext', './globe/EarthElevationModel', './util/FrameStatistics', @@ -59,6 +60,7 @@ define([ function (ArgumentError, BasicWorldWindowController, Camera, + LookAt, DrawContext, EarthElevationModel, FrameStatistics, @@ -142,6 +144,15 @@ define([ // Internal. Intentionally not documented. this.scratchProjection = Matrix.fromIdentity(); + // Internal. Intentionally not documented. + this.scratchPoint = new Vec3(0, 0, 0); + + // Internal. Intentionally not documented. + this.scratchPosition = new Position(0, 0, 0); + + // Internal. Intentionally not documented. + this.scratchRay = new Line(new Vec3(0, 0, 0), new Vec3(0, 0, 0)); + // Internal. Intentionally not documented. this.hasStencilBuffer = gl.getContextAttributes().stencil; @@ -196,7 +207,7 @@ define([ * @type {Camera} * @default [Camera]{@link Camera} */ - this.camera = new Camera(this); + this.camera = new Camera(); /** * The controller used to manipulate the globe. @@ -498,6 +509,113 @@ define([ this.redrawRequested = true; // redraw during the next animation frame }; + /** + * Sets the properties of this Camera such that it mimics the supplied look at view. Note that repeated conversions + * between a look at and a camera view may result in view errors due to rounding. + * @param {LookAt} lookAt The look at view to mimic. + * @returns {Camera} This camera set to mimic the supplied look at view. + * @throws {ArgumentError} If the specified look at view is null or undefined. + */ + WorldWindow.prototype.cameraFromLookAt = function (lookAt) { + if (!lookAt) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Camera", "setFromLookAt", "missingLookAt")); + } + + var globe = this.globe, + ve = this.verticalExaggeration, + position = this.camera.position, + ray = this.scratchRay, + originPoint = this.scratchPoint, + modelview = this.scratchModelview, + origin = this.scratchProjection; + + this.lookAtToViewingTransform(lookAt, modelview); + modelview.extractEyePoint(originPoint); + + globe.computePositionFromPoint(originPoint[0], originPoint[1], originPoint[2], position); + origin.setToIdentity(); + origin.multiplyByLocalCoordinateTransform(originPoint, globe); + modelview.multiplyMatrix(origin); + + this.camera.heading = modelview.extractHeading(lookAt.roll); // disambiguate heading and roll + this.camera.tilt = modelview.extractTilt(); + this.camera.roll = lookAt.roll; // roll passes straight through + + // Check if camera altitude is not under the surface and correct tilt + var elevation = globe.elevationAtLocation(position.latitude, position.longitude) * ve + 10.0; // 10m above surface + if(elevation > position.altitude) { + // Set camera altitude above the surface + position.altitude = elevation; + // Compute new camera point + globe.computePointFromPosition(position.latitude, position.longitude, position.altitude, originPoint); + // Compute look at point + globe.computePointFromPosition(lookAt.position.latitude, lookAt.position.longitude, lookAt.position.altitude, ray.origin); + // Compute normal to globe in look at point + globe.surfaceNormalAtLocation(lookAt.position.latitude, lookAt.position.longitude, ray.direction); + // Calculate tilt angle between new camera point and look at point + originPoint.subtract(ray.origin).normalize(); + var dot = ray.direction.dot(originPoint); + if (dot >= -1 && dot <= 1) { + this.camera.tilt = Math.acos(dot) / Math.PI * 180; + } + } + + return this; + }; + + /** + * Converts the properties of this Camera to those of a look at view. Note that repeated conversions + * between a look at and a camera view may result in view errors due to rounding. + * @param {LookAt} result The look at view to hold the converted properties. + * @param {Position} terrainPosition Picked terrain position. + * @returns {LookAt} A reference to the result parameter. + * @throws {ArgumentError} If the specified result object is null or undefined. + */ + WorldWindow.prototype.cameraAsLookAt = function (result, terrainPosition = this.pick([this.viewport.width / 2, this.viewport.height / 2]).terrainObject().position) { + if (!result) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Camera", "getAsLookAt", "missingResult")); + } + + var globe = this.globe, + forwardRay = this.scratchRay, + modelview = this.scratchModelview, + originPoint = this.scratchPoint, + originPos = this.scratchPosition, + origin = this.scratchProjection; + + this.cameraToViewingTransform(modelview); + + // Pick terrain located behind the viewport center point + if (terrainPosition) { + // Use picked terrain position including approximate rendered altitude + originPos.copy(terrainPosition); + globe.computePointFromPosition(originPos.latitude, originPos.longitude, originPos.altitude, originPoint); + } else { + // Center is outside the globe - use point on horizon + modelview.extractEyePoint(forwardRay.origin); + modelview.extractForwardVector(forwardRay.direction); + + var horizon = globe.horizonDistance(this.camera.position.altitude); + forwardRay.pointAt(horizon, originPoint); + + globe.computePositionFromPoint(originPoint[0], originPoint[1], originPoint[2], originPos); + } + + origin.setToIdentity(); + origin.multiplyByLocalCoordinateTransform(originPoint, globe); + modelview.multiplyMatrix(origin); + + result.position.copy(originPos); + result.range = -modelview[11]; + result.heading = modelview.extractHeading(this.camera.roll); // disambiguate heading and roll + result.tilt = modelview.extractTilt(); + result.roll = this.camera.roll; // roll passes straight through + + return result; + }; + /** * Requests the WorldWind objects displayed at a specified screen-coordinate point. * @@ -710,7 +828,7 @@ define([ Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "computeViewingTransform", "missingModelview")); } - this.camera.computeViewingTransform(modelview); + this.cameraToViewingTransform(modelview); if (projection) { var eyePos = this.camera.position, @@ -753,6 +871,45 @@ define([ } }; + /** + * Internal use only. + * Computes the model view matrix for this camera. + * @ignore + */ + WorldWindow.prototype.cameraToViewingTransform = function (modelview) { + if (!modelview) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Camera", "cameraToViewingTransform", "missingModelview")); + } + + modelview.setToIdentity(); + modelview.multiplyByFirstPersonModelview(this.camera.position, this.camera.heading, this.camera.tilt, this.camera.roll, this.globe); + + return modelview; + }; + + /** + * Internal use only. + * Computes the model view matrix for this look at view. + * @ignore + */ + WorldWindow.prototype.lookAtToViewingTransform = function (lookAt, modelview) { + if (!lookAt) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "lookAtToViewingTransform", "missingGlobe")); + } + + if (!modelview) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "WorldWindow", "lookAtToViewingTransform", "missingModelview")); + } + + modelview.setToIdentity(); + modelview.multiplyByLookAtModelview(lookAt.position, lookAt.range, lookAt.heading, lookAt.tilt, lookAt.roll, this.globe); + + return modelview; + }; + /** * Computes the approximate size of a pixel at a specified distance from the eye point. *

diff --git a/src/WorldWindowController.js b/src/WorldWindowController.js index 2a6974616..1e9f3c3d9 100644 --- a/src/WorldWindowController.js +++ b/src/WorldWindowController.js @@ -124,14 +124,6 @@ define([ } }; - /** - * Called by WorldWindow to allow the controller to enforce navigation limits. Implementation is not required by - * sub-classes. - */ - WorldWindowController.prototype.applyLimits = function () { - - }; - return WorldWindowController; } ); diff --git a/src/geom/Camera.js b/src/geom/Camera.js index 8276cb1da..a4dc60fc3 100644 --- a/src/geom/Camera.js +++ b/src/geom/Camera.js @@ -18,37 +18,15 @@ */ define([ '../error/ArgumentError', - '../geom/Line', '../util/Logger', - '../geom/LookAt', - '../geom/Matrix', - '../geom/Position', - '../geom/Vec3', - '../util/WWMath' + '../geom/Position' ], function (ArgumentError, - Line, Logger, - LookAt, - Matrix, - Position, - Vec3, - WWMath) { + Position) { "use strict"; - var Camera = function (worldWindow) { - if (!worldWindow) { - throw new ArgumentError( - Logger.logMessage(Logger.LEVEL_SEVERE, "Camera", "constructor", "missingWorldWindow")); - } - - /** - * The WorldWindow associated with this camera. - * @type {WorldWindow} - * @readonly - */ - this.wwd = worldWindow; - + var Camera = function () { /** * The geographic location of the camera. * @type {Location} @@ -81,63 +59,6 @@ define([ * @default 45 */ this.fieldOfView = 45; - - /** - * Internal use only. - * A temp variable used to hold model view matrices during calculations. Using an object level temp property - * negates the need for ad-hoc allocations and reduces load on the garbage collector. - * @ignore - */ - this.scratchModelview = Matrix.fromIdentity(); - - /** - * Internal use only. - * A temp variable used to hold points during calculations. Using an object level temp property - * negates the need for ad-hoc allocations and reduces load on the garbage collector. - * @ignore - */ - this.scratchPoint = new Vec3(0, 0, 0); - - /** - * Internal use only. - * A temp variable used to hold origin matrices during calculations. Using an object level temp property - * negates the need for ad-hoc allocations and reduces load on the garbage collector. - * @ignore - */ - this.scratchOrigin = Matrix.fromIdentity(); - - /** - * Internal use only. - * A temp variable used to hold positions during calculations. Using an object level temp property - * negates the need for ad-hoc allocations and reduces load on the garbage collector. - * @ignore - */ - this.scratchPosition = new Position(0, 0, 0); - - /** - * Internal use only. - * A temp variable used to hold lines during calculations. Using an object level temp property - * negates the need for ad-hoc allocations and reduces load on the garbage collector. - * @ignore - */ - this.scratchRay = new Line(new Vec3(0, 0, 0), new Vec3(0, 0, 0)); - }; - - /** - * Internal use only. - * Computes the model view matrix for this camera. - * @ignore - */ - Camera.prototype.computeViewingTransform = function (modelview) { - if (!modelview) { - throw new ArgumentError( - Logger.logMessage(Logger.LEVEL_SEVERE, "Camera", "computeViewingTransform", "missingModelview")); - } - - modelview.setToIdentity(); - modelview.multiplyByFirstPersonModelview(this.position, this.heading, this.tilt, this.roll, this.wwd.globe); - - return modelview; }; /** @@ -163,7 +84,7 @@ define([ * @returns {Camera} The new object. */ Camera.prototype.clone = function () { - var clone = new Camera(this.wwd); + var clone = new Camera(); clone.copy(this); return clone; @@ -181,7 +102,6 @@ define([ Logger.logMessage(Logger.LEVEL_SEVERE, "Camera", "copy", "missingObject")); } - this.wwd = copyObject.wwd; this.position.copy(copyObject.position); this.heading = copyObject.heading; this.tilt = copyObject.tilt; @@ -190,113 +110,6 @@ define([ return this; }; - /** - * Sets the properties of this Camera such that it mimics the supplied look at view. Note that repeated conversions - * between a look at and a camera view may result in view errors due to rounding. - * @param {LookAt} lookAt The look at view to mimic. - * @returns {Camera} This camera set to mimic the supplied look at view. - * @throws {ArgumentError} If the specified look at view is null or undefined. - */ - Camera.prototype.setFromLookAt = function (lookAt) { - if (!lookAt) { - throw new ArgumentError( - Logger.logMessage(Logger.LEVEL_SEVERE, "Camera", "setFromLookAt", "missingLookAt")); - } - - var globe = this.wwd.globe, - ve = this.wwd.verticalExaggeration, - ray = this.scratchRay, - originPoint = this.scratchPoint, - modelview = this.scratchModelview, - origin = this.scratchOrigin; - - lookAt.computeViewingTransform(globe, modelview); - modelview.extractEyePoint(originPoint); - - globe.computePositionFromPoint(originPoint[0], originPoint[1], originPoint[2], this.position); - origin.setToIdentity(); - origin.multiplyByLocalCoordinateTransform(originPoint, globe); - modelview.multiplyMatrix(origin); - - this.heading = modelview.extractHeading(lookAt.roll); // disambiguate heading and roll - this.tilt = modelview.extractTilt(); - this.roll = lookAt.roll; // roll passes straight through - - // Check if camera altitude is not under the surface and correct tilt - var elevation = globe.elevationAtLocation(this.position.latitude, this.position.longitude) * ve + 10.0; // 10m above surface - if(elevation > this.position.altitude) { - // Set camera altitude above the surface - this.position.altitude = elevation; - // Compute new camera point - globe.computePointFromPosition(this.position.latitude, this.position.longitude, this.position.altitude, originPoint); - // Compute look at point - globe.computePointFromPosition(lookAt.position.latitude, lookAt.position.longitude, lookAt.position.altitude, ray.origin); - // Compute normal to globe in look at point - globe.surfaceNormalAtLocation(lookAt.position.latitude, lookAt.position.longitude, ray.direction); - // Calculate tilt angle between new camera point and look at point - originPoint.subtract(ray.origin).normalize(); - var dot = ray.direction.dot(originPoint); - if (dot >= -1 || dot <= 1) { - this.tilt = Math.acos(dot) / Math.PI * 180; - } - } - - return this; - }; - - /** - * Converts the properties of this Camera to those of a look at view. Note that repeated conversions - * between a look at and a camera view may result in view errors due to rounding. - * @param {LookAt} result The look at view to hold the converted properties. - * @returns {LookAt} A reference to the result parameter. - * @throws {ArgumentError} If the specified result object is null or undefined. - */ - Camera.prototype.getAsLookAt = function (result) { - if (!result) { - throw new ArgumentError( - Logger.logMessage(Logger.LEVEL_SEVERE, "Camera", "getAsLookAt", "missingResult")); - } - - var globe = this.wwd.globe, - viewport = this.wwd.viewport, - forwardRay = this.scratchRay, - modelview = this.scratchModelview, - originPoint = this.scratchPoint, - originPos = this.scratchPosition, - origin = this.scratchOrigin; - - this.computeViewingTransform(modelview); - - // Pick terrain located behind the viewport center point - var terrainObject = this.wwd.pick([viewport.width / 2, viewport.height / 2]).terrainObject(); - if (terrainObject) { - // Use picked terrain position including approximate rendered altitude - originPos.copy(terrainObject.position); - globe.computePointFromPosition(originPos.latitude, originPos.longitude, originPos.altitude, originPoint); - } else { - // Center is outside the globe - use point on horizon - modelview.extractEyePoint(forwardRay.origin); - modelview.extractForwardVector(forwardRay.direction); - - var horizon = globe.horizonDistance(this.position.altitude); - forwardRay.pointAt(horizon, originPoint); - - globe.computePositionFromPoint(originPoint[0], originPoint[1], originPoint[2], originPos); - } - - origin.setToIdentity(); - origin.multiplyByLocalCoordinateTransform(originPoint, globe); - modelview.multiplyMatrix(origin); - - result.position.copy(originPos); - result.range = -modelview[11]; - result.heading = modelview.extractHeading(this.roll); // disambiguate heading and roll - result.tilt = modelview.extractTilt(); - result.roll = this.roll; // roll passes straight through - - return result; - }; - /** * Returns a string representation of this object. * @returns {String} @@ -306,5 +119,4 @@ define([ }; return Camera; - }); - + }); \ No newline at end of file diff --git a/src/geom/LookAt.js b/src/geom/LookAt.js index f023c15fb..e6c6c63b0 100644 --- a/src/geom/LookAt.js +++ b/src/geom/LookAt.js @@ -19,12 +19,10 @@ define([ '../error/ArgumentError', '../util/Logger', - '../geom/Matrix', '../geom/Position' ], function (ArgumentError, Logger, - Matrix, Position) { "use strict"; @@ -64,28 +62,6 @@ define([ this.range = 10e6; // TODO: Compute initial range to fit globe in viewport. }; - /** - * Internal use only. - * Computes the model view matrix for this look at view. - * @ignore - */ - LookAt.prototype.computeViewingTransform = function (globe, modelview) { - if (!globe) { - throw new ArgumentError( - Logger.logMessage(Logger.LEVEL_SEVERE, "LookAt", "computeViewingTransform", "missingGlobe")); - } - - if (!modelview) { - throw new ArgumentError( - Logger.logMessage(Logger.LEVEL_SEVERE, "LookAt", "computeViewingTransform", "missingModelview")); - } - - modelview.setToIdentity(); - modelview.multiplyByLookAtModelview(this.position, this.range, this.heading, this.tilt, this.roll, globe); - - return modelview; - }; - /** * Indicates whether the components of this object are equal to those of a specified object. * @param {LookAt} otherLookAt The object to test equality with. May be null or undefined, in which case this diff --git a/src/layer/ViewControlsLayer.js b/src/layer/ViewControlsLayer.js index 631d2f6cf..ae39bdf03 100644 --- a/src/layer/ViewControlsLayer.js +++ b/src/layer/ViewControlsLayer.js @@ -697,7 +697,7 @@ define([ if (this.isPointerDown(e) || this.isTouchStart(e)) { this.activeControl = control; this.activeOperation = this.handlePan; - this.wwd.camera.getAsLookAt(this.lookAt); + this.wwd.cameraAsLookAt(this.lookAt); e.preventDefault(); if (this.isTouchStart(e)) { @@ -722,7 +722,7 @@ define([ Location.greatCircleLocation(lookAt.position, heading, -distance, lookAt.position); - thisLayer.wwd.camera.setFromLookAt(lookAt); + thisLayer.wwd.cameraFromLookAt(lookAt); thisLayer.wwd.redraw(); setTimeout(setLookAtLocation, 50); } @@ -743,7 +743,7 @@ define([ if (this.isPointerDown(e) || this.isTouchStart(e)) { this.activeControl = control; this.activeOperation = this.handleZoom; - this.wwd.camera.getAsLookAt(this.lookAt); + this.wwd.cameraAsLookAt(this.lookAt); e.preventDefault(); if (this.isTouchStart(e)) { @@ -760,7 +760,7 @@ define([ } else if (thisLayer.activeControl === thisLayer.zoomOutControl) { lookAt.range *= (1 + thisLayer.zoomIncrement); } - thisLayer.wwd.camera.setFromLookAt(lookAt); + thisLayer.wwd.cameraFromLookAt(lookAt); thisLayer.wwd.redraw(); setTimeout(setRange, 50); } @@ -781,7 +781,7 @@ define([ if (this.isPointerDown(e) || this.isTouchStart(e)) { this.activeControl = control; this.activeOperation = this.handleHeading; - this.wwd.camera.getAsLookAt(this.lookAt); + this.wwd.cameraAsLookAt(this.lookAt); e.preventDefault(); if (this.isTouchStart(e)) { @@ -798,7 +798,7 @@ define([ } else if (thisLayer.activeControl === thisLayer.headingRightControl) { lookAt.heading -= thisLayer.headingIncrement; } - thisLayer.wwd.camera.setFromLookAt(lookAt); + thisLayer.wwd.cameraFromLookAt(lookAt); thisLayer.wwd.redraw(); setTimeout(setHeading, 50); } @@ -818,7 +818,7 @@ define([ if (this.isPointerDown(e) || this.isTouchStart(e)) { this.activeControl = control; this.activeOperation = this.handleTilt; - this.wwd.camera.getAsLookAt(this.lookAt); + this.wwd.cameraAsLookAt(this.lookAt); e.preventDefault(); if (this.isTouchStart(e)) { @@ -837,7 +837,7 @@ define([ lookAt.tilt = Math.min(90, lookAt.tilt + thisLayer.tiltIncrement); } - thisLayer.wwd.camera.setFromLookAt(lookAt); + thisLayer.wwd.cameraFromLookAt(lookAt); thisLayer.wwd.redraw(); setTimeout(setTilt, 50); } diff --git a/src/navigate/LookAtNavigator.js b/src/navigate/LookAtNavigator.js index dfe80798c..1f6198318 100644 --- a/src/navigate/LookAtNavigator.js +++ b/src/navigate/LookAtNavigator.js @@ -71,12 +71,12 @@ define([ */ lookAtLocation: { get: function () { - this.wwd.camera.getAsLookAt(this.scratchLookAt); + this.wwd.cameraAsLookAt(this.scratchLookAt); this.scratchLookAtPositionProxy.position.copy(this.scratchLookAt.position); return this.scratchLookAtPositionProxy; }, set: function (value) { - var lookAt = this.wwd.camera.getAsLookAt(this.scratchLookAt); + var lookAt = this.wwd.cameraAsLookAt(this.scratchLookAt); lookAt.position.latitude = value.latitude; lookAt.position.longitude = value.longitude; if (value.altitude) { @@ -85,7 +85,7 @@ define([ else { lookAt.position.altitude = 0; } - this.wwd.camera.setFromLookAt(lookAt); + this.wwd.cameraFromLookAt(lookAt); } }, @@ -96,12 +96,12 @@ define([ */ range: { get: function () { - return this.wwd.camera.getAsLookAt(this.scratchLookAt).range; + return this.wwd.cameraAsLookAt(this.scratchLookAt).range; }, set: function (value) { - var lookAt = this.wwd.camera.getAsLookAt(this.scratchLookAt); + var lookAt = this.wwd.cameraAsLookAt(this.scratchLookAt); lookAt.range = value; - this.wwd.camera.setFromLookAt(lookAt); + this.wwd.cameraFromLookAt(lookAt); } } }); diff --git a/src/navigate/Navigator.js b/src/navigate/Navigator.js index 8319bf5c4..a3a990b8c 100644 --- a/src/navigate/Navigator.js +++ b/src/navigate/Navigator.js @@ -71,12 +71,12 @@ define(['../error/ArgumentError', */ heading: { get: function () { - return this.wwd.camera.getAsLookAt(this.scratchLookAt).heading; + return this.wwd.cameraAsLookAt(this.scratchLookAt).heading; }, set: function (value) { - var lookAt = this.wwd.camera.getAsLookAt(this.scratchLookAt); + var lookAt = this.wwd.cameraAsLookAt(this.scratchLookAt); lookAt.heading = value; - this.wwd.camera.setFromLookAt(lookAt); + this.wwd.cameraFromLookAt(lookAt); } }, @@ -87,12 +87,12 @@ define(['../error/ArgumentError', */ tilt: { get: function () { - return this.wwd.camera.getAsLookAt(this.scratchLookAt).tilt; + return this.wwd.cameraAsLookAt(this.scratchLookAt).tilt; }, set: function (value) { - var lookAt = this.wwd.camera.getAsLookAt(this.scratchLookAt); + var lookAt = this.wwd.cameraAsLookAt(this.scratchLookAt); lookAt.tilt = value; - this.wwd.camera.setFromLookAt(lookAt); + this.wwd.cameraFromLookAt(lookAt); } }, @@ -103,12 +103,12 @@ define(['../error/ArgumentError', */ roll: { get: function () { - return this.wwd.camera.getAsLookAt(this.scratchLookAt).roll; + return this.wwd.cameraAsLookAt(this.scratchLookAt).roll; }, set: function (value) { - var lookAt = this.wwd.camera.getAsLookAt(this.scratchLookAt); + var lookAt = this.wwd.cameraAsLookAt(this.scratchLookAt); lookAt.roll = value; - this.wwd.camera.setFromLookAt(lookAt); + this.wwd.cameraFromLookAt(lookAt); } } }); diff --git a/src/util/GoToAnimator.js b/src/util/GoToAnimator.js index 3feced89b..60081ec96 100644 --- a/src/util/GoToAnimator.js +++ b/src/util/GoToAnimator.js @@ -121,7 +121,7 @@ define([ // Reset the cancellation flag. this.cancelled = false; - this.wwd.camera.getAsLookAt(this.lookAt); + this.wwd.cameraAsLookAt(this.lookAt); // Capture the target position and determine its altitude. this.targetPosition = new Position(position.latitude, position.longitude, position.altitude || this.lookAt.range); @@ -260,7 +260,7 @@ define([ continueAnimation = Math.abs(this.lookAt.range - this.targetPosition.altitude) > 1; } - this.wwd.camera.setFromLookAt(this.lookAt); + this.wwd.cameraFromLookAt(this.lookAt); return continueAnimation; }; @@ -280,7 +280,7 @@ define([ this.lookAt.position.latitude = nextLocation.latitude; this.lookAt.position.longitude = nextLocation.longitude; - this.wwd.camera.setFromLookAt(this.lookAt); + this.wwd.cameraFromLookAt(this.lookAt); // We're done if we're within a meter of the desired location. if (nextDistance < 1 / this.wwd.globe.equatorialRadius) { diff --git a/src/util/KeyboardControls.js b/src/util/KeyboardControls.js index 9e178594e..cfa6a22a3 100644 --- a/src/util/KeyboardControls.js +++ b/src/util/KeyboardControls.js @@ -120,9 +120,9 @@ define([ * Reset the view to North up. */ KeyboardControls.prototype.resetHeading = function () { - this.wwd.camera.getAsLookAt(this.lookAt); + this.wwd.cameraAsLookAt(this.lookAt); this.lookAt.heading = Number(0); - this.wwd.camera.setFromLookAt(this.lookAt); + this.wwd.cameraFromLookAt(this.lookAt); this.wwd.redraw(); }; @@ -130,10 +130,10 @@ define([ * Reset the view to North up and nadir. */ KeyboardControls.prototype.resetHeadingAndTilt = function () { - this.wwd.camera.getAsLookAt(this.lookAt); + this.wwd.cameraAsLookAt(this.lookAt); this.lookAt.heading = 0; this.lookAt.tilt = 0; - this.wwd.camera.setFromLookAt(this.lookAt); + this.wwd.cameraFromLookAt(this.lookAt); this.wwd.redraw(); }; @@ -154,7 +154,7 @@ define([ */ KeyboardControls.prototype.handleZoom = function (operation) { this.activeOperation = this.handleZoom; - this.wwd.camera.getAsLookAt(this.lookAt); + this.wwd.cameraAsLookAt(this.lookAt); // This function is called by the timer to perform the operation. var self = this, // capture 'this' for use in the function @@ -165,7 +165,7 @@ define([ } else if (operation === "zoomOut") { self.lookAt.range *= (1 + self.zoomIncrement); } - self.wwd.camera.setFromLookAt(self.lookAt); + self.wwd.cameraFromLookAt(self.lookAt); self.wwd.redraw(); setTimeout(setRange, 50); } @@ -179,7 +179,7 @@ define([ */ KeyboardControls.prototype.handlePan = function (operation) { this.activeOperation = this.handlePan; - this.wwd.camera.getAsLookAt(this.lookAt); + this.wwd.cameraAsLookAt(this.lookAt); // This function is called by the timer to perform the operation. var self = this, // capture 'this' for use in the function @@ -207,7 +207,7 @@ define([ heading, distance, self.lookAt.position); - self.wwd.camera.setFromLookAt(self.lookAt); + self.wwd.cameraFromLookAt(self.lookAt); self.wwd.redraw(); setTimeout(setLookAtLocation, 50); } diff --git a/test/BasicWorldWindowController.test.js b/test/BasicWorldWindowController.test.js index 1a08bdb89..e8f962049 100644 --- a/test/BasicWorldWindowController.test.js +++ b/test/BasicWorldWindowController.test.js @@ -48,7 +48,7 @@ define([ // wwd.worldWindowController.handlePanOrDrag2D(recognizer); // // var lookAt = new LookAt(); - // wwd.camera.getAsLookAt(lookAt); + // wwd.cameraAsLookAt(lookAt); // // expect(lookAt.range).toEqual(10000000); // expect(lookAt.tilt).toEqual(0); diff --git a/test/geom/Camera.test.js b/test/geom/Camera.test.js index 4d0d19029..45132ddd6 100644 --- a/test/geom/Camera.test.js +++ b/test/geom/Camera.test.js @@ -36,10 +36,10 @@ define([ // var lookAt = new LookAt(); // camera.position = new Position(30, -110, 10000000); // for (var a = 0; a < 90; a++) { - // camera.getAsLookAt(lookAt); + // wwd.cameraAsLookAt(lookAt); // console.log(lookAt.toString()); // lookAt.heading = a; - // camera.setFromLookAt(lookAt); + // wwd.cameraFromLookAt(lookAt); // console.log(camera.toString()); // console.log('==='); // } @@ -48,7 +48,7 @@ define([ // var testView = wwd.camera; // testView.position = new Position(30, -110, 10e6); // var result = Matrix.fromIdentity(); - // testView.computeViewingTransform(result); + // wwd.cameraToViewingTransform(result); // var expectedModelview = new Matrix( // -0.3420201433256687, 0.0, 0.9396926207859083, 0.0, // 0.46984631039295405, 0.8660254037844386, 0.17101007166283433, 18504.157, @@ -61,7 +61,7 @@ define([ // var testView = wwd.camera; // testView.position = new Position(0, 0, 10e6); // var result = Matrix.fromIdentity(); - // testView.computeViewingTransform(result); + // wwd.cameraToViewingTransform(result); // var expectedModelview = new Matrix( // 1.0, 0.0, 0.0, -0.0, // 0.0, 1.0, 0.0, -0.0, @@ -73,7 +73,7 @@ define([ // var testView = wwd.camera; // testView.position = new Position(30, 0, 10e6); // var result = Matrix.fromIdentity(); - // testView.computeViewingTransform(result); + // wwd.cameraToViewingTransform(result); // var expectedModelview = new Matrix( // 1.0,0.0,0.0,-0.0, // 0.0,0.8660254037844387,-0.5,18504.125313225202, @@ -87,7 +87,7 @@ define([ // var lookAt = new LookAt(); // lookAt.range = 1.131761199603698E7; // lookAt.position = new Position(30, -90, 0); - // camera.setFromLookAt(lookAt); + // wwd.cameraFromLookAt(lookAt); // expect(camera.position.latitude).toBeCloseTo(30.0, 6); // expect(camera.position.longitude).toBeCloseTo(-90.0, 6); // expect(camera.position.altitude).toBeCloseTo(1.131761199603698E7, 6); @@ -104,7 +104,7 @@ define([ // lookAt.roll = 5; // lookAt.heading = 15; // lookAt.position = new Position(30, -90, 0); - // camera.setFromLookAt(lookAt); + // wwd.cameraFromLookAt(lookAt); // expect(camera.position.latitude).toBeCloseTo(26.90254740059172, 6); // expect(camera.position.longitude).toBeCloseTo(-90.92754733364956, 6); // expect(camera.position.altitude).toBeCloseTo(11302122.347, 3); @@ -117,14 +117,14 @@ define([ describe("Indicates whether the components of two cameras are equal", function () { it("Equal cameras", function () { - var c1 = new Camera("test"); - var c2 = new Camera("test"); + var c1 = new Camera(); + var c2 = new Camera(); expect(c1.equals(c2)).toBe(true); }); it("Not equal cameras", function () { - var c1 = new Camera("test"); - var c2 = new Camera("test"); + var c1 = new Camera(); + var c2 = new Camera(); c2.heading = c1.heading + 1; expect(c1.equals(c2)).toBe(false); c2.heading = c1.heading; @@ -144,7 +144,7 @@ define([ }); it("Null comparison", function () { - var c1 = new Camera("test"); + var c1 = new Camera(); expect(c1.equals(null)).toBe(false); expect(c1.equals(undefined)).toBe(false); }); @@ -152,8 +152,8 @@ define([ describe("Camera cloning and copying", function () { it("Correctly copy cameras", function () { - var c1 = new Camera("test"); - var c2 = new Camera("test"); + var c1 = new Camera(); + var c2 = new Camera(); c2.heading = c1.heading + 1; c2.tilt = c1.tilt + 1; c2.roll = c1.roll + 1; @@ -163,7 +163,7 @@ define([ }); it("Correctly clones cameras", function () { - var c1 = new Camera("test"); + var c1 = new Camera(); c1.heading = c1.heading + 1; c1.tilt = c1.tilt + 1; c1.roll = c1.roll + 1; diff --git a/test/geom/LookAt.test.js b/test/geom/LookAt.test.js index c119c32ec..a261612c9 100644 --- a/test/geom/LookAt.test.js +++ b/test/geom/LookAt.test.js @@ -34,7 +34,7 @@ define([ lookAt.position = new Position(30, -90, 0); lookAt.range = 1.131761199603698E7; var result = Matrix.fromIdentity(); - lookAt.computeViewingTransform(wwd.globe, result); + wwd.lookAtToViewingTransform(lookAt, result); var expectedModelview = new Matrix( 6.123233995736767E-17, -3.0814879110195774E-33, 1.0, 2.0679515313825692E-25, 0.5, 0.8660254037844387, -3.0616169978683836E-17, 18504.125313223805, diff --git a/test/util/TestUtils.test.js b/test/util/TestUtils.test.js index 525ce4a0a..c3642b041 100644 --- a/test/util/TestUtils.test.js +++ b/test/util/TestUtils.test.js @@ -81,7 +81,7 @@ define([ var wwd = new MockWorldWindow(); wwd.globe = mockGlobe; wwd.drawContext = dc; - wwd.camera = new Camera(wwd); + wwd.camera = new Camera(); wwd.worldWindowController = new BasicWorldWindowController(wwd); wwd.viewport = viewport; wwd.depthBits = 24; From 163aa21f8965d7868f8797a6e39f2c9811aab251 Mon Sep 17 00:00:00 2001 From: Eugene Maksymenko Date: Wed, 28 Sep 2022 00:55:59 +0300 Subject: [PATCH 5/6] Fix code to be ES5 compatible. --- src/WorldWindow.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/WorldWindow.js b/src/WorldWindow.js index 8cb44e58b..6a4b37ffa 100644 --- a/src/WorldWindow.js +++ b/src/WorldWindow.js @@ -572,11 +572,17 @@ define([ * @returns {LookAt} A reference to the result parameter. * @throws {ArgumentError} If the specified result object is null or undefined. */ - WorldWindow.prototype.cameraAsLookAt = function (result, terrainPosition = this.pick([this.viewport.width / 2, this.viewport.height / 2]).terrainObject().position) { + WorldWindow.prototype.cameraAsLookAt = function (result, terrainPosition) { if (!result) { throw new ArgumentError( Logger.logMessage(Logger.LEVEL_SEVERE, "Camera", "getAsLookAt", "missingResult")); } + if (!terrainPosition) { + var terrainObject = this.pick([this.viewport.width / 2, this.viewport.height / 2]).terrainObject(); + if (terrainObject) { + terrainPosition = terrainObject.position; + } + } var globe = this.globe, forwardRay = this.scratchRay, From ef2f7df19172e87c8fda7334ba0290f70eaf46e7 Mon Sep 17 00:00:00 2001 From: Eugene Maksymenko Date: Wed, 28 Sep 2022 12:21:38 +0300 Subject: [PATCH 6/6] Keep map scale by adopting field of view on view port resize --- src/BasicWorldWindowController.js | 2 +- src/WorldWindow.js | 15 ++++++++++++--- src/geom/Camera.js | 23 +++++++++++++++++++++-- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/BasicWorldWindowController.js b/src/BasicWorldWindowController.js index 48b519a1e..0590006bd 100644 --- a/src/BasicWorldWindowController.js +++ b/src/BasicWorldWindowController.js @@ -457,7 +457,7 @@ define([ // Clamp tilt to between 0 and +90 to prevent the viewer from going upside down. lookAt.tilt = WWMath.clamp(lookAt.tilt, 0, 90); - // Normalize heading to between -180 and +180. + // Normalize roll to between -180 and +180. lookAt.roll = Angle.normalizedDegrees(lookAt.roll); // Apply 2D limits when the globe is 2D. diff --git a/src/WorldWindow.js b/src/WorldWindow.js index 6a4b37ffa..e126d6654 100644 --- a/src/WorldWindow.js +++ b/src/WorldWindow.js @@ -175,7 +175,7 @@ define([ * @type {Rectangle} * @readonly */ - this.viewport = new Rectangle(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); + this.viewport = new Rectangle(0, 0, 0, 0); /** * The globe displayed. @@ -811,13 +811,22 @@ define([ width = gl.canvas.clientWidth * this.pixelScale, height = gl.canvas.clientHeight * this.pixelScale; - if (gl.canvas.width != width || - gl.canvas.height != height) { + if (gl.canvas.width != width || gl.canvas.height != height + || this.viewport.width === 0 || this.viewport.height === 0) { // Make the canvas drawing buffer size match its screen size. gl.canvas.width = width; gl.canvas.height = height; + // Keep map scale by adopting field of view on view port resize + if (this.viewport.height !== 0) { + try { + this.camera.fieldOfView *= height / this.viewport.height; + } catch (ignore) { + // Keep original field of view in case new one does not fit requirements + } + } + // 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); diff --git a/src/geom/Camera.js b/src/geom/Camera.js index a4dc60fc3..29313d35a 100644 --- a/src/geom/Camera.js +++ b/src/geom/Camera.js @@ -42,6 +42,7 @@ define([ /** * Camera tilt, in degrees. + * @type {Number} * @default 0 */ this.tilt = 0; @@ -53,13 +54,31 @@ define([ */ this.roll = 0; + // Intentionally not documented + this._fieldOfView = 45; + }; + + Object.defineProperties(Camera.prototype, { /** * Camera vertical field of view, in degrees * @type {Number} * @default 45 + * @throws {ArgumentError} If the specified field of view is out of range. */ - this.fieldOfView = 45; - }; + fieldOfView: { + get: function () { + return this._fieldOfView; + }, + set: function (value) { + if (value <= 0 || value >= 180) { + throw new ArgumentError( + Logger.logMessage(Logger.LEVEL_SEVERE, "Camera", "setFieldOfView", "Invalid field of view") + ); + } + this._fieldOfView = value; + } + } + }); /** * Indicates whether the components of this object are equal to those of a specified object.