Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rotatable 2D map #3990

Merged
merged 10 commits into from
Jun 1, 2016
47 changes: 28 additions & 19 deletions Source/Scene/Camera.js
Original file line number Diff line number Diff line change
@@ -186,18 +186,11 @@ define([
this.constrainedAxis = undefined;
/**
* The factor multiplied by the the map size used to determine where to clamp the camera position
* when translating across the surface. The default is 1.5. Only valid for 2D and Columbus view.
* when zooming out from the surface. The default is 1.5. Only valid for 2D and the map is rotatable.
* @type {Number}
* @default 1.5
*/
this.maximumTranslateFactor = 1.5;
/**
* The factor multiplied by the the map size used to determine where to clamp the camera position
* when zooming out from the surface. The default is 2.5. Only valid for 2D.
* @type {Number}
* @default 2.5
*/
this.maximumZoomFactor = 2.5;
this.maximumZoomFactor = 1.5;

this._moveStart = new Event();
this._moveEnd = new Event();
@@ -1165,20 +1158,32 @@ define([
};

function clampMove2D(camera, position) {
var maxX = camera._maxCoord.x;
if (position.x > maxX) {
position.x = position.x - maxX * 2.0;
var rotatable2D = camera._scene.rotatable2D;
var maxProjectedX = camera._maxCoord.x;
var maxProjectedY = camera._maxCoord.y;

var minX;
var maxX;
if (rotatable2D) {
maxX = maxProjectedX;
minX = -maxX;
} else {
maxX = position.x - maxProjectedX * 2.0;
minX = position.x + maxProjectedX * 2.0;
}
if (position.x < -maxX) {
position.x = position.x + maxX * 2.0;

if (position.x > maxProjectedX) {
position.x = maxX;
}
if (position.x < -maxProjectedX) {
position.x = minX;
}

var maxY = camera._maxCoord.y;
if (position.y > maxY) {
position.y = maxY;
if (position.y > maxProjectedY) {
position.y = maxProjectedY;
}
if (position.y < -maxY) {
position.y = -maxY;
if (position.y < -maxProjectedY) {
position.y = -maxProjectedY;
}
}

@@ -1536,6 +1541,10 @@ define([
var newLeft = frustum.left + amount;

var maxRight = camera._maxCoord.x;
if (camera._scene.rotatable2D) {
maxRight *= camera.maximumZoomFactor;
}

if (newRight > maxRight) {
newRight = maxRight;
newLeft = -maxRight;
15 changes: 14 additions & 1 deletion Source/Scene/Scene.js
Original file line number Diff line number Diff line change
@@ -185,6 +185,7 @@ define([
* @param {Boolean} [options.scene3DOnly=false] If true, optimizes memory use and performance for 3D mode but disables the ability to use 2D or Columbus View.
* @param {Number} [options.terrainExaggeration=1.0] A scalar used to exaggerate the terrain. Note that terrain exaggeration will not modify any other primitive as they are positioned relative to the ellipsoid.
* @param {Boolean} [options.shadows=false] Determines if shadows are cast by the sun.
* @param {Boolean} [options.rotatable2D=false] Determines if the 2D map is rotatable or can be scrolled infinitely in the horizontal direction.
*
* @see CesiumWidget
* @see {@link http://www.khronos.org/registry/webgl/specs/latest/#5.2|WebGLContextAttributes}
@@ -573,6 +574,7 @@ define([
this._camera = camera;
this._cameraClone = Camera.clone(camera);
this._screenSpaceCameraController = new ScreenSpaceCameraController(this);
this._rotatable2D = defaultValue(options.rotatable2D, false);

// Keeps track of the state of a frame. FrameState is the state across
// the primitives of the scene. This state is for internally keeping track
@@ -1062,6 +1064,17 @@ define([
this._camera.frustum.xOffset = 0.0;
}
}
},

/**
* Determines if the 2D map is rotatable or can be scrolled infinitely in the horizontal direction.
* @memberof Scene.prototype
* @type {Boolean}
*/
rotatable2D : {
get : function() {
return this._rotatable2D;
}
}
});

@@ -1940,7 +1953,7 @@ define([
viewport.width = context.drawingBufferWidth;
viewport.height = context.drawingBufferHeight;

if (mode !== SceneMode.SCENE2D) {
if (mode !== SceneMode.SCENE2D || scene._rotatable2D) {
executeCommandsInViewport(true, scene, passState, backgroundColor, picking);
} else {
execute2DViewportCommands(scene, passState, backgroundColor, picking);
70 changes: 69 additions & 1 deletion Source/Scene/ScreenSpaceCameraController.js
Original file line number Diff line number Diff line change
@@ -594,12 +594,80 @@ define([
handleZoom(controller, startPosition, movement, controller._zoomFactor, camera.getMagnitude());
}

var twist2DStart = new Cartesian2();
var twist2DEnd = new Cartesian2();

function twist2D(controller, startPosition, movement) {
if (defined(movement.angleAndHeight)) {
singleAxisTwist2D(controller, startPosition, movement.angleAndHeight);
return;
}

var scene = controller._scene;
var camera = scene.camera;
var canvas = scene.canvas;
var width = canvas.clientWidth;
var height = canvas.clientHeight;

var start = twist2DStart;
start.x = (2.0 / width) * movement.startPosition.x - 1.0;
start.y = (2.0 / height) * (height - movement.startPosition.y) - 1.0;
start = Cartesian2.normalize(start, start);

var end = twist2DEnd;
end.x = (2.0 / width) * movement.endPosition.x - 1.0;
end.y = (2.0 / height) * (height - movement.endPosition.y) - 1.0;
end = Cartesian2.normalize(end, end);

var startTheta = CesiumMath.acosClamped(start.x);
if (start.y < 0) {
startTheta = CesiumMath.TWO_PI - startTheta;
}
var endTheta = CesiumMath.acosClamped(end.x);
if (end.y < 0) {
endTheta = CesiumMath.TWO_PI - endTheta;
}
var theta = endTheta - startTheta;

camera.twistRight(theta);
}

function singleAxisTwist2D(controller, startPosition, movement) {
var rotateRate = controller._rotateFactor * controller._rotateRateRangeAdjustment;

if (rotateRate > controller._maximumRotateRate) {
rotateRate = controller._maximumRotateRate;
}

if (rotateRate < controller._minimumRotateRate) {
rotateRate = controller._minimumRotateRate;
}

var scene = controller._scene;
var camera = scene.camera;
var canvas = scene.canvas;

var phiWindowRatio = (movement.endPosition.x - movement.startPosition.x) / canvas.clientWidth;
phiWindowRatio = Math.min(phiWindowRatio, controller.maximumMovementRatio);

var deltaPhi = rotateRate * phiWindowRatio * Math.PI * 4.0;

camera.twistRight(deltaPhi);
}

function update2D(controller) {
var rotatable2D = controller._scene.rotatable2D;
if (!Matrix4.equals(Matrix4.IDENTITY, controller._scene.camera.transform)) {
reactToInput(controller, controller.enableZoom, controller.zoomEventTypes, zoom2D, controller.inertiaZoom, '_lastInertiaZoomMovement');
if (rotatable2D) {
reactToInput(controller, controller.enableRotate, controller.translateEventTypes, twist2D, controller.inertiaSpin, '_lastInertiaSpinMovement');
}
} else {
reactToInput(controller, controller.enableTranslate, controller.translateEventTypes, translate2D, controller.inertiaTranslate, '_lastInertiaTranslateMovement');
reactToInput(controller, controller.enableZoom, controller.zoomEventTypes, zoom2D, controller.inertiaZoom, '_lastInertiaZoomMovement');
if (rotatable2D) {
reactToInput(controller, controller.enableRotate, controller.tiltEventTypes, twist2D, controller.inertiaSpin, '_lastInertiaTiltMovement');
}
}
}

@@ -1821,7 +1889,7 @@ define([
*
* @example
* controller = controller && controller.destroy();
*
*
* @see ScreenSpaceCameraController#isDestroyed
*/
ScreenSpaceCameraController.prototype.destroy = function() {
4 changes: 3 additions & 1 deletion Source/Widgets/CesiumWidget/CesiumWidget.js
Original file line number Diff line number Diff line change
@@ -161,6 +161,7 @@ define([
* @param {Number} [options.terrainExaggeration=1.0] A scalar used to exaggerate the terrain. Note that terrain exaggeration will not modify any other primitive as they are positioned relative to the ellipsoid.
* @param {Boolean} [options.shadows=false] Determines if shadows are cast by the sun.
* @param {Boolean} [options.terrainShadows=false] Determines if the terrain casts shadows from the sun.
* @param {Boolean} [options.rotatable2D=false] Determines if the 2D map is rotatable or can be scrolled infinitely in the horizontal direction.
*
* @exception {DeveloperError} Element with id "container" does not exist in the document.
*
@@ -258,7 +259,8 @@ define([
orderIndependentTranslucency : options.orderIndependentTranslucency,
scene3DOnly : defaultValue(options.scene3DOnly, false),
terrainExaggeration : options.terrainExaggeration,
shadows : options.shadows
shadows : options.shadows,
rotatable2D : options.rotatable2D
});
this._scene = scene;

4 changes: 3 additions & 1 deletion Source/Widgets/Viewer/Viewer.js
Original file line number Diff line number Diff line change
@@ -278,6 +278,7 @@ define([
* @param {Number} [options.terrainExaggeration=1.0] A scalar used to exaggerate the terrain. Note that terrain exaggeration will not modify any other primitive as they are positioned relative to the ellipsoid.
* @param {Boolean} [options.shadows=false] Determines if shadows are cast by the sun.
* @param {Boolean} [options.terrainShadows=false] Determines if the terrain casts shadows from the sun.
* @param {Boolean} [options.rotatable2D=false] Determines if the 2D map is rotatable or can be scrolled infinitely in the horizontal direction.
*
* @exception {DeveloperError} Element with id "container" does not exist in the document.
* @exception {DeveloperError} options.imageryProvider is not available when using the BaseLayerPicker widget, specify options.selectedImageryProviderViewModel instead.
@@ -412,7 +413,8 @@ Either specify options.terrainProvider instead or set options.baseLayerPicker to
scene3DOnly : scene3DOnly,
terrainExaggeration : options.terrainExaggeration,
shadows : options.shadows,
terrainShadows : options.terrainShadows
terrainShadows : options.terrainShadows,
rotatable2D : options.rotatable2D
});

var dataSourceCollection = options.dataSources;
58 changes: 58 additions & 0 deletions Specs/Scene/ScreenSpaceCameraControllerSpec.js
Original file line number Diff line number Diff line change
@@ -87,6 +87,7 @@ defineSuite([
});

afterEach(function() {
scene.rotatable2D = false;
controller = controller && !controller.isDestroyed() && controller.destroy();
});

@@ -446,6 +447,63 @@ defineSuite([
expect(camera.frustum.bottom).toEqual(-camera.frustum.top);
});

it('rotate counter-clockwise in 2D', function() {
setUp2D();
scene.rotatable2D = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this property readonly?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this overwrites the property get function with the boolean? Perhaps it is more obvious to assign to _ rotatable2D?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't a real Scene instance. A MockScene is created before each test.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, OK.


var position = Cartesian3.clone(camera.position);
var startPosition = new Cartesian2(canvas.clientWidth / 4, canvas.clientHeight / 2);
var endPosition = new Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 4);

moveMouse(MouseButtons.MIDDLE, startPosition, endPosition);
updateController();
expect(position.x).toEqual(camera.position.x);
expect(position.y).toEqual(camera.position.y);
expect(position.z).toEqual(camera.position.z);

expect(camera.direction).toEqualEpsilon(Cartesian3.negate(Cartesian3.UNIT_Z, new Cartesian3()), CesiumMath.EPSILON15);
expect(camera.up).toEqualEpsilon(Cartesian3.negate(Cartesian3.UNIT_X, new Cartesian3()), CesiumMath.EPSILON15);
expect(camera.right).toEqualEpsilon(Cartesian3.UNIT_Y, CesiumMath.EPSILON15);
});

it('rotate clockwise in 2D', function() {
setUp2D();
scene.rotatable2D = true;

var position = Cartesian3.clone(camera.position);
var startPosition = new Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 4);
var endPosition = new Cartesian2(canvas.clientWidth / 4, canvas.clientHeight / 2);

moveMouse(MouseButtons.MIDDLE, startPosition, endPosition);
updateController();
expect(position.x).toEqual(camera.position.x);
expect(position.y).toEqual(camera.position.y);
expect(position.z).toEqual(camera.position.z);

expect(camera.direction).toEqualEpsilon(Cartesian3.negate(Cartesian3.UNIT_Z, new Cartesian3()), CesiumMath.EPSILON15);
expect(camera.up).toEqualEpsilon(Cartesian3.UNIT_X, CesiumMath.EPSILON15);
expect(camera.right).toEqualEpsilon(Cartesian3.negate(Cartesian3.UNIT_Y, new Cartesian3()), CesiumMath.EPSILON15);
});

it('rotates counter-clockwise with mouse position at bottom of the screen', function() {
setUp2D();
scene.rotatable2D = true;

var position = Cartesian3.clone(camera.position);
var startPosition = new Cartesian2(3 * canvas.clientWidth / 4, 3 * canvas.clientHeight / 4);
var endPosition = new Cartesian2(canvas.clientWidth / 4, 3 * canvas.clientHeight / 4);

moveMouse(MouseButtons.MIDDLE, startPosition, endPosition);
updateController();
expect(position.x).toEqual(camera.position.x);
expect(position.y).toEqual(camera.position.y);
expect(position.z).toEqual(camera.position.z);

expect(camera.direction).toEqualEpsilon(Cartesian3.negate(Cartesian3.UNIT_Z, new Cartesian3()), CesiumMath.EPSILON15);
expect(camera.up).toEqualEpsilon(Cartesian3.negate(Cartesian3.UNIT_X, new Cartesian3()), CesiumMath.EPSILON15);
expect(camera.right).toEqualEpsilon(Cartesian3.UNIT_Y, CesiumMath.EPSILON15);
});

it('translate right in Columbus view', function() {
setUpCV();
var position = Cartesian3.clone(camera.position);