diff --git a/examples/tiles/index.pug b/examples/tiles/index.pug index a0de4d3d67..9bd60c95fc 100644 --- a/examples/tiles/index.pug +++ b/examples/tiles/index.pug @@ -88,7 +88,7 @@ block append mainContent .form-group(title="The camera can use a parallel or perspective projection. The difference is subtly unless the data has non-zero z-values.") label(for="camera-projection") Camera Projection - select#camera-projection.cameraparam(param-name="projection", placeholder="parallel") + select#camera-projection.cameraparam(param-name="projection", placeholder="parallel", reload="true") option(value="parallel") Parallel option(value="perspective") Perspective diff --git a/examples/tiles/main.js b/examples/tiles/main.js index f242505340..40169f4ad9 100644 --- a/examples/tiles/main.js +++ b/examples/tiles/main.js @@ -349,7 +349,7 @@ $(function () { } break; } - if (ctl.is('.layerparam') && ctl.attr('reload') === 'true') { + if (ctl.is('.layerparam,.cameraparam') && ctl.attr('reload') === 'true') { map.deleteLayer(osmLayer); osmLayer = map.createLayer('osm', layerParams); tileDebug.osmLayer = osmLayer; diff --git a/src/camera.js b/src/camera.js index 9de55a29c1..4a7a570c08 100644 --- a/src/camera.js +++ b/src/camera.js @@ -120,30 +120,10 @@ var camera = function (spec) { * @protected */ this._createProj = function () { - var s = this.constructor.bounds.near / this.constructor.bounds.far; - - // call mat4.frustum or mat4.ortho here - if (this._projection === 'perspective') { - mat4.frustum( - this._proj, - this.constructor.bounds.left * s, - this.constructor.bounds.right * s, - this.constructor.bounds.bottom * s, - this.constructor.bounds.top * s, - -this.constructor.bounds.near, - -this.constructor.bounds.far - ); - } else if (this._projection === 'parallel') { - mat4.ortho( - this._proj, - this.constructor.bounds.left, - this.constructor.bounds.right, - this.constructor.bounds.bottom, - this.constructor.bounds.top, - this.constructor.bounds.near, - this.constructor.bounds.far - ); - } + var func = this._projection === 'perspective' ? mat4.frustum : mat4.ortho, + clipbounds = this._clipbounds[this._projection]; + func(this._proj, clipbounds.left, clipbounds.right, clipbounds.bottom, + clipbounds.top, clipbounds.near, clipbounds.far); }; /** @@ -156,7 +136,7 @@ var camera = function (spec) { this._bounds = null; this._display = null; this._world = null; - this._transform = camera.combine(this._proj, this._view); + this._transform = mat4.multiply(util.mat4AsArray(), this._proj, this._view); mat4.invert(this._inverse, this._transform); this.geoTrigger(geo_event.camera.view, { camera: this @@ -203,40 +183,81 @@ var camera = function (spec) { }); /** - * Getter for the "display" matrix. This matrix converts from - * world coordinates into display coordinates. This matrix exists to - * generate matrix3d css transforms that can be used in layers that - * render on the DOM. Read only. + * Getter/setter for the render clipbounds. Opposite bounds must have + * different values. There are independant clipbounds for each projection + * (parallel and perspective); switching the projection will switch to the + * clipbounds. Individual values of the clipbounds can be set either via a + * command like `camera.clipbounds = {near: 3, far: 1}` or + * `camera.clipbounds.near = 3`. In the second example, no check is made to + * ensure a non-zero volume clipbounds. + * + * @property {object} clipbounds The clipbounds for the current projection. + * @name geo.camera#clipbounds + */ + Object.defineProperty(this, 'clipbounds', { + get: function () { + return this._clipbounds[this._projection]; + }, + set: function (bounds) { + var clipbounds = this._clipbounds[this._projection]; + bounds = { + left: bounds.left === undefined ? clipbounds.left : bounds.left, + right: bounds.right === undefined ? clipbounds.right : bounds.right, + top: bounds.top === undefined ? clipbounds.top : bounds.top, + bottom: bounds.bottom === undefined ? clipbounds.bottom : bounds.bottom, + near: bounds.near === undefined ? clipbounds.near : bounds.near, + far: bounds.far === undefined ? clipbounds.far : bounds.far + }; + if (bounds.left === bounds.right) { + throw new Error('Left and right values must be different'); + } + if (bounds.top === bounds.bottom) { + throw new Error('Top and bottom values must be different'); + } + if (bounds.near === bounds.far) { + throw new Error('Near and far values must be different'); + } + this._clipbounds[this._projection] = bounds; + this._createProj(); + this._update(); + } + }); + + /** + * Getter for the "display" matrix. This matrix converts from world + * coordinates into display coordinates. Read only. * * @property {mat4} display The display matrix. * @name geo.camera#display */ Object.defineProperty(this, 'display', { get: function () { - var mat; if (this._display === null) { - mat = camera.affine( - {x: 1, y: 1}, // translate to: [0, 2] x [0, 2] - { - x: this.viewport.width / 2, - y: this.viewport.height / -2 - } // scale to: [0, width] x [-height, 0] - ); - - // applies mat to the transform (world -> normalized) - this._display = camera.combine( - mat, - this._transform - ); + var b = this._clipbounds[this._projection]; + var mat = util.mat4AsArray(); + mat4.translate(mat, mat, [ + this.viewport.width / 2, + this.viewport.height / 2, + 0]); + mat4.scale(mat, mat, [ + this.viewport.width / (b.right - b.left), + this.viewport.height / (b.bottom - b.top), + 1]); + mat4.translate(mat, mat, [ + -(b.left + b.right) / 2, + -(b.top + b.bottom) / 2, + 0]); + mat4.multiply(mat, mat, this._transform); + this._display = mat; } return this._display; } }); /** - * Getter for the "world" matrix. This matrix converts from - * display coordinates into world coordinates. This is constructed - * by inverting the "display" matrix. Read only. + * Getter for the "world" matrix. This matrix converts from display + * coordinates into world coordinates. This is the inverse of the "display" + * matrix. Read only. * * @property {mat4} world The world matrix. * @name geo.camera#world @@ -422,7 +443,7 @@ var camera = function (spec) { * @returns {vec4} The point in clip space coordinates. */ this._worldToClip4 = function (point) { - return camera.applyTransform(this._transform, point); + return vec4.transformMat4(point, point, this._transform); }; /** @@ -432,7 +453,7 @@ var camera = function (spec) { * @returns {vec4} The point in world space coordinates. */ this._clipToWorld4 = function (point) { - return camera.applyTransform(this._inverse, point); + return vec4.transformMat4(point, point, this._inverse); }; /** @@ -474,92 +495,63 @@ var camera = function (spec) { }; /** - * Project a vec4 from world space into viewport space. - * @param {vec4} point The point in world coordinates (mutated). - * @returns {vec4} The point in display coordinates. + * Project a vector from world space into viewport (display) space. The + * resulting vector always has the last component (`w`) equal to 1. * - * @note For the moment, this computation assumes the following: - * * point[3] > 0 - * * depth range [0, 1] - * - * The clip space z and w coordinates are returned with the window - * x/y coordinates. + * @param {vec2|vec3|vec4} point The point in world coordinates. + * @returns {vec4} The point in display coordinates. */ this.worldToDisplay4 = function (point) { - // This is because z = 0 is the far plane exposed to the user, but - // internally the far plane is at -2. - point[2] -= 2; - - // convert to clip space - this._worldToClip4(point); - - // apply projection specific transformation - point = this.applyProjection(point); - - // convert to display space - point[0] = this._viewport.width * (1 + point[0]) / 2.0; - point[1] = this._viewport.height * (1 - point[1]) / 2.0; - point[2] = (1 + point[2]) / 2.0; + point = [point[0], point[1], point[2] || 0, point[3] || 1]; + point = vec4.transformMat4(point, point, this.display); + if (point[3] && point[3] !== 1) { + point = [point[0] / point[3], point[1] / point[3], point[2] / point[3], 1]; + } return point; }; /** - * Project a vec4 from display space into world space in place. - * @param {vec4} point The point in display coordinates (mutated). - * @returns {vec4} The point in world space coordinates. + * Project a vector from viewport (display) space into world space. The + * resulting vector always has the last component (`w`) equal to 1. * - * @note For the moment, this computation assumes the following: - * * point[3] > 0 - * * depth range [0, 1] + * @param {vec2|vec3|vec4} point The point in display coordinates. + * @returns {vec4} The point in world space coordinates. */ this.displayToWorld4 = function (point) { - // convert to clip space - point[0] = 2 * point[0] / this._viewport.width - 1; - point[1] = -2 * point[1] / this._viewport.height + 1; - point[2] = 2 * point[2] - 1; - - // invert projection transform - point = this.unapplyProjection(point); - - // convert to world coordinates - this._clipToWorld4(point); - - // move far surface to z = 0 - point[2] += 2; + point = [point[0], point[1], point[2] || 0, point[3] || 1]; + point = vec4.transformMat4(point, point, this.world); + if (point[3] && point[3] !== 1) { + point = [point[0] / point[3], point[1] / point[3], point[2] / point[3], 1]; + } return point; }; /** - * Project a point object from world space into viewport space. + * Project a 2D point object from world space into viewport space. `z` is + * set to `-this.clipbounds.near` to scale with the clip space. + * * @param {object} point The point in world coordinates. * @param {number} point.x * @param {number} point.y * @returns {object} The point in display coordinates. */ this.worldToDisplay = function (point) { - // define some magic numbers: - var z = 0, // z coordinate of the surface in world coordinates - w = 1; // enables perspective divide (i.e. for point conversion) - point = this.worldToDisplay4( - [point.x, point.y, z, w] - ); + var b = this._clipbounds[this._projection]; + point = this.worldToDisplay4([point.x, point.y, -b.near]); return {x: point[0], y: point[1]}; }; /** - * Project a point object from viewport space into world space. + * Project a 2D point object from viewport space into world space. `z` is + * set to -1 to scale with the clip space. + * * @param {object} point The point in display coordinates. * @param {number} point.x * @param {number} point.y * @returns {object} The point in world coordinates. */ this.displayToWorld = function (point) { - // define some magic numbers: - var z = 1, // the z coordinate of the surface - w = 2; // perspective divide at z = 1 - point = this.displayToWorld4( - [point.x, point.y, z, w] - ); + point = this.displayToWorld4([point.x, point.y, -1]); return {x: point[0], y: point[1]}; }; @@ -578,10 +570,7 @@ var camera = function (spec) { ul = this.displayToWorld({x: 0, y: 0}); ur = this.displayToWorld({x: this._viewport.width, y: 0}); ll = this.displayToWorld({x: 0, y: this._viewport.height}); - lr = this.displayToWorld({ - x: this._viewport.width, - y: this._viewport.height - }); + lr = this.displayToWorld({x: this._viewport.width, y: this._viewport.height}); bds.left = Math.min(ul.x, ur.x, ll.x, lr.x); bds.bottom = Math.min(ul.y, ur.y, ll.y, lr.y); @@ -768,19 +757,13 @@ var camera = function (spec) { /** * Returns a CSS transform that converts (by default) from world coordinates - * into display coordinates. This allows users of this module to - * position elements using world coordinates directly inside DOM - * elements. + * into display coordinates. This allows users of this module to position + * elements using world coordinates directly inside DOM elements. This + * expects that the transform-origin is 0 0. * - * @note This transform will not take into account projection specific - * transforms. For perspective projections, one can use the properties - * `perspective` and `perspective-origin` to apply the projection - * in css directly. - * - * @param {string} transform The transform to return - * * display - * * world - * @returns {string} The css transform string + * @param {string} [transform='display'] The transform to return. One of + * `display` or `world`. + * @returns {string} The css transform string. */ this.css = function (transform) { var m; @@ -855,6 +838,7 @@ var camera = function (spec) { return this._transform; }; + this._clipbounds = this.constructor.clipbounds; // initialize the view matrix this._resetView(); @@ -881,103 +865,49 @@ camera.projection = { }; /** - * Camera clipping bounds, probably shouldn't be modified. + * Default camera clipping bounds. Some features and renderers may rely on the + * far clip value being more positive than the near clip value. */ -camera.bounds = { - left: -1, - right: 1, - top: 1, - bottom: -1, - far: -2, - near: -1 +camera.clipbounds = { + perspective: { + left: -1, + right: 1, + top: 1, + bottom: -1, + far: 2000, + near: 0.01 + }, + parallel: { + left: -1, + right: 1, + top: 1, + bottom: -1, + far: -1, + near: 1 + } }; /** - * Output a mat4 as a css transform. + * Output a mat4 as a css transform. This expects that the transform-origin is + * 0 0. + * * @param {mat4} t A matrix transform. * @returns {string} A css transform string. */ camera.css = function (t) { - return ( - 'matrix3d(' + - [ - t[0].toFixed(20), - t[1].toFixed(20), - t[2].toFixed(20), - t[3].toFixed(20), - t[4].toFixed(20), - t[5].toFixed(20), - t[6].toFixed(20), - t[7].toFixed(20), - t[8].toFixed(20), - t[9].toFixed(20), - t[10].toFixed(20), - t[11].toFixed(20), - t[12].toFixed(20), - t[13].toFixed(20), - t[14].toFixed(20), - t[15].toFixed(20) - ].join(',') + - ')' - ); -}; - -/** - * Generate a mat4 representing an affine coordinate transformation. - * - * For the following affine transform: - * - * x |-> m * (x + a) + b - * - * applies the css transform: - * - * translate(b) scale(m) translate(a) . - * - * If a parameter is `null` or `undefined`, that component is skipped. - * - * @param {object?} pre Coordinate offset **before** scaling. - * @param {object?} scale Coordinate scaling. - * @param {object?} post Coordinate offset **after** scaling. - * @returns {mat4} The new transform matrix. - */ -camera.affine = function (pre, scale, post) { - var mat = util.mat4AsArray(); - - // Note: mat4 operations are applied to the right side of the current - // transform, so the first applied here is the last applied to the - // coordinate. - if (post) { - mat4.translate(mat, mat, [post.x || 0, post.y || 0, post.z || 0]); - } - if (scale) { - mat4.scale(mat, mat, [scale.x || 1, scale.y || 1, scale.z || 1]); - } - if (pre) { - mat4.translate(mat, mat, [pre.x || 0, pre.y || 0, pre.z || 0]); - } - return mat; -}; - -/** - * Apply the given transform matrix to a point in place. - * @param {mat4} t - * @param {vec4} pt - * @returns {vec4} - */ -camera.applyTransform = function (t, pt) { - return vec4.transformMat4(pt, pt, t); -}; - -/** - * Combine two transforms by multiplying their matrix representations. - * @note The second transform provided will be the first applied in the - * coordinate transform. - * @param {mat4} A - * @param {mat4} B - * @returns {mat4} A * B - */ -camera.combine = function (A, B) { - return mat4.multiply(util.mat4AsArray(), A, B); + return 'matrix3d(' + + t.map(function (val) { + /* Format each value with a certain precision, but don't use scientific + * notation or keep needless trailing zeroes. */ + val = (+val).toPrecision(15); + if (val.indexOf('e') >= 0) { + val = (+val).toString(); + } else if (val.indexOf('.') >= 0) { + val = val.replace(/(\.|)0+$/, ''); + } + return val; + }).join(',') + + ')'; }; inherit(camera, object); diff --git a/src/canvas/tileLayer.js b/src/canvas/tileLayer.js index f3dbbf91e0..9659063d37 100644 --- a/src/canvas/tileLayer.js +++ b/src/canvas/tileLayer.js @@ -34,7 +34,7 @@ var canvas_tileLayer = function () { quad.lr = this.fromLocal(this.fromLevel({ x: bounds.right - to.x, y: bounds.bottom - to.y }, level), 0); - quad.ul.z = quad.ll.z = quad.ur.z = quad.lr.z = level * 1e-5; + quad.ul.z = quad.ll.z = quad.ur.z = quad.lr.z = level * m_this._levelZIncrement; m_nextTileId += 1; quad.id = m_nextTileId; tile.quadId = quad.id; diff --git a/src/d3/tileLayer.js b/src/d3/tileLayer.js index f85518585d..697470fd30 100644 --- a/src/d3/tileLayer.js +++ b/src/d3/tileLayer.js @@ -29,7 +29,7 @@ var d3_tileLayer = function () { quad.lr = this.fromLocal(this.fromLevel({ x: bounds.right - to.x, y: bounds.bottom - to.y }, level), 0); - quad.ul.z = quad.ll.z = quad.ur.z = quad.lr.z = level * 1e-5; + quad.ul.z = quad.ll.z = quad.ur.z = quad.lr.z = level * m_this._levelZIncrement; m_nextTileId += 1; quad.id = m_nextTileId; tile.quadId = quad.id; diff --git a/src/gl/tileLayer.js b/src/gl/tileLayer.js index a5d9f83526..28fe4a76f3 100644 --- a/src/gl/tileLayer.js +++ b/src/gl/tileLayer.js @@ -37,7 +37,12 @@ var gl_tileLayer = function () { quad.lr = this.fromLocal(this.fromLevel({ x: bounds.right - to.x, y: bounds.bottom - to.y }, level), 0); - quad.ul.z = quad.ll.z = quad.ur.z = quad.lr.z = level * 1e-5; + /* Make sure our level increments are within the clipbounds and ordered so + * that lower levels are farther away that higher levels. */ + var clipbounds = m_this.map().camera().clipbounds; + var z = level * m_this._levelZIncrement; + z = clipbounds.far + (clipbounds.near - clipbounds.far) * z; + quad.ul.z = quad.ll.z = quad.ur.z = quad.lr.z = z; m_nextTileId += 1; quad.id = m_nextTileId; tile.quadId = quad.id; diff --git a/src/gl/vglRenderer.js b/src/gl/vglRenderer.js index c337e3289c..b7229b36b7 100644 --- a/src/gl/vglRenderer.js +++ b/src/gl/vglRenderer.js @@ -227,14 +227,20 @@ var vglRenderer = function (arg) { view = camera.view, proj = camera.projectionMatrix; if (proj[15]) { - /* we want positive z to be closer to the camera, but webGL does the - * converse, so reverse the z coordinates. */ - proj = mat4.scale(util.mat4AsArray(), proj, [1, 1, -1]); + /* In the parallel projection, we want the clipbounds [near, far] to map + * to [0, 1]. The ortho matrix scales to [-1, 1]. */ + proj = mat4.copy(util.mat4AsArray(), proj); + proj = mat4.scale(proj, proj, [1, 1, -0.5]); + proj = mat4.translate(proj, proj, [0, 0, camera.clipbounds.far]); + } else { + /* This rescales the perspective projection to work with most gl + * features. It doesn't work with all clipbounds, and will probably need + * to be refactored when we have tiltable maps. */ + var near = camera.clipbounds.near, + far = camera.clipbounds.far; + proj = mat4.copy(util.mat4AsArray(), proj); + proj = mat4.scale(proj, proj, [1 / near, 1 / near, -1 / far]); } - /* A similar kluge as in the base camera class worldToDisplay4. With this, - * we can show z values from 0 to 1. */ - proj = mat4.translate(util.mat4AsArray(), proj, - [0, 0, camera.constructor.bounds.far]); /* Check if the rotation is a multiple of 90 */ var basis = Math.PI / 2, angle = rotation % basis, // move to range (-pi/2, pi/2) diff --git a/src/tileLayer.js b/src/tileLayer.js index aeea6093a9..ea316b850e 100644 --- a/src/tileLayer.js +++ b/src/tileLayer.js @@ -196,6 +196,11 @@ var tileLayer = function (options) { m_maxBounds = [], m_exited; + // Space tile levels this far apart in z-buffer space. This value doesn't + // have any visual effect in parallel projections, but will slightly skew + // perspective projections. + this._levelZIncrement = 1e-5; + // copy the options into a private variable this._options = $.extend(true, {}, options); diff --git a/tests/cases/camera.js b/tests/cases/camera.js index f8bb28fee1..d2eadda4ff 100644 --- a/tests/cases/camera.js +++ b/tests/cases/camera.js @@ -4,10 +4,10 @@ var $ = require('jquery'); var mat4 = require('gl-mat4'); -var vec3 = require('gl-vec3'); var vec4 = require('gl-vec4'); var geo = require('../test-utils').geo; var closeToArray = require('../test-utils').closeToArray; +var closeToEqual = require('../test-utils').closeToEqual; describe('geo.camera', function () { 'use strict'; @@ -21,10 +21,10 @@ describe('geo.camera', function () { c.bounds = b; b1 = c.bounds; - expect(b.left).toBe(b1.left); - expect(b.right).toBe(b1.right); - expect(b.top).toBe(b1.top); - expect(b.bottom).toBe(b1.bottom); + expect(b1.left).toBeCloseTo(b.left); + expect(b1.right).toBeCloseTo(b.right); + expect(b1.top).toBeCloseTo(b.top); + expect(b1.bottom).toBeCloseTo(b.bottom); }; } function testcase(proj) { @@ -47,28 +47,13 @@ describe('geo.camera', function () { }); describe('resize viewport', function () { - function number_near(n1, n2, tol) { - return Math.abs(n1 - n2) < tol; - } - function bounds_near(b1, b2, tol) { - tol = tol || 1e-4; - var n = number_near(b1.left, b2.left, tol) && - number_near(b1.right, b2.right, tol) && - number_near(b1.bottom, b2.bottom, tol) && - number_near(b1.top, b2.top, tol); - if (!n) { - console.log(JSON.stringify(b1) + ' != ' + JSON.stringify(b2)); - } - return n; - } - it('100 x 100 -> 90 x 90', function () { var c = geo.camera({viewport: {width: 100, height: 100}}); c.bounds = {left: 0, right: 100, bottom: 0, top: 100}; c.viewport = {width: 90, height: 90}; - expect(bounds_near(c.bounds, {left: 5, right: 95, bottom: 5, top: 95})) + expect(closeToEqual(c.bounds, {left: 5, right: 95, bottom: 5, top: 95}, 4)) .toBe(true); }); it('100 x 100 -> 100 x 90', function () { @@ -77,7 +62,7 @@ describe('geo.camera', function () { c.bounds = {left: 0, right: 100, bottom: 0, top: 100}; c.viewport = {width: 100, height: 90}; - expect(bounds_near(c.bounds, {left: 0, right: 100, bottom: 5, top: 95})) + expect(closeToEqual(c.bounds, {left: 0, right: 100, bottom: 5, top: 95}, 4)) .toBe(true); }); it('100 x 100 -> 20 x 10', function () { @@ -86,7 +71,7 @@ describe('geo.camera', function () { c.bounds = {left: 0, right: 100, bottom: 0, top: 100}; c.viewport = {width: 20, height: 10}; - expect(bounds_near(c.bounds, {left: 40, right: 60, bottom: 45, top: 55})) + expect(closeToEqual(c.bounds, {left: 40, right: 60, bottom: 45, top: 55}, 4)) .toBe(true); }); it('100 x 100 -> 140 x 120', function () { @@ -95,7 +80,7 @@ describe('geo.camera', function () { c.bounds = {left: 0, right: 100, bottom: 0, top: 100}; c.viewport = {width: 140, height: 120}; - expect(bounds_near(c.bounds, {left: -20, right: 120, bottom: -10, top: 110})) + expect(closeToEqual(c.bounds, {left: -20, right: 120, bottom: -10, top: 110}, 4)) .toBe(true); }); it('50 x 100 -> 100 x 100', function () { @@ -104,7 +89,7 @@ describe('geo.camera', function () { c.bounds = {left: 0, right: 50, bottom: 0, top: 100}; c.viewport = {width: 100, height: 100}; - expect(bounds_near(c.bounds, {left: -25, right: 75, bottom: 0, top: 100})) + expect(closeToEqual(c.bounds, {left: -25, right: 75, bottom: 0, top: 100}, 4)) .toBe(true); }); it('50 x 50 -> 100 x 100', function () { @@ -113,7 +98,7 @@ describe('geo.camera', function () { c.bounds = {left: 0, right: 50, bottom: 0, top: 50}; c.viewport = {width: 100, height: 100}; - expect(bounds_near(c.bounds, {left: -25, right: 75, bottom: -25, top: 75})) + expect(closeToEqual(c.bounds, {left: -25, right: 75, bottom: -25, top: 75}, 4)) .toBe(true); }); }); @@ -364,6 +349,7 @@ describe('geo.camera', function () { position: 'absolute', width: '100px', height: '100px', + 'transform-origin': '0 0', transform: 'none' /*,border: '1px solid black'*/ }); @@ -380,21 +366,15 @@ describe('geo.camera', function () { bottom: node.position().top + box.height, top: node.position().top, left: node.position().left, - right: node.position().left + box.width, - height: box.height, - width: box.width + right: node.position().left + box.width }; } function assert_position(position) { - var _ = get_node_position(), k, actual = {}; - for (k in position) { - if (position.hasOwnProperty(k)) { - position[k] = position[k].toFixed(2); - actual[k] = _[k].toFixed(2); - } - } - expect(actual).toEqual(position); + if (!closeToEqual(get_node_position(), position, 2)) { //DWM:: + console.log('assert_position', get_node_position(), position); //DWM:: + } //DWM:: + expect(closeToEqual(get_node_position(), position, 2)).toBe(true); } it('Display and world parameters', function () { @@ -413,24 +393,19 @@ describe('geo.camera', function () { camera = geo.camera(); camera.viewport = {width: 100, height: 100}; - camera.bounds = geo.camera.bounds; - camera.pan({x: 10, y: 0}); + camera.bounds = geo.camera.clipbounds.parallel; + camera.pan({x: 10, y: 0}); node.css('transform', geo.camera.css(camera.view)); - expect(node.position().left).toBe(10); - expect(node.position().top).toBe(0); + assert_position({left: 10, top: 0, right: 110, bottom: 100}); camera.pan({x: 0, y: -5}); - node.css('transform', geo.camera.css(camera.view)); - expect(node.position().left).toBe(10); - expect(node.position().top).toBe(-5); + assert_position({left: 10, top: -5, right: 110, bottom: 95}); camera.pan({x: -10, y: 5}); - node.css('transform', geo.camera.css(camera.view)); - expect(node.position().left).toBe(0); - expect(node.position().top).toBe(0); + assert_position({left: 0, top: 0, right: 100, bottom: 100}); }); it('Simple zooming', function () { var node = make_node(), @@ -438,50 +413,40 @@ describe('geo.camera', function () { view; camera.viewport = {width: 100, height: 100}; - camera.bounds = geo.camera.bounds; + camera.bounds = geo.camera.clipbounds.parallel; view = camera.view; camera.zoom(1); expect(camera.view).toBe(view); camera.zoom(0.5); - node.css('transform', geo.camera.css(camera.view)); - expect(node.position().left).toBe(25); - expect(node.position().top).toBe(25); + assert_position({left: 0, top: 0, right: 50, bottom: 50}); camera.zoom(6); - node.css('transform', geo.camera.css(camera.view)); - expect(node.position().left).toBe(-100); - expect(node.position().top).toBe(-100); + assert_position({left: 0, top: 0, right: 300, bottom: 300}); camera.zoom(1 / 3); - node.css('transform', geo.camera.css(camera.view)); - expect(node.position().left).toBe(0); - expect(node.position().top).toBe(0); + assert_position({left: 0, top: 0, right: 100, bottom: 100}); }); it('Zooming + panning', function () { var node = make_node(), camera = geo.camera(); camera.viewport = {width: 100, height: 100}; - camera.bounds = geo.camera.bounds; + camera.bounds = geo.camera.clipbounds.parallel; camera.pan({x: -25, y: -25}); camera.zoom(0.5); - node.css('transform', geo.camera.css(camera.view)); - expect(node.position().left).toBe(0); - expect(node.position().top).toBe(0); + assert_position({left: -25, top: -25, right: 25, bottom: 25}); camera.pan({x: 50, y: 50}); camera.zoom(2); - node.css('transform', geo.camera.css(camera.view)); - expect(node.position().left).toBe(0); - expect(node.position().top).toBe(0); + assert_position({left: 0, top: 0, right: 100, bottom: 100}); }); it('Simple rotation', function () { var node = make_node(), @@ -489,7 +454,7 @@ describe('geo.camera', function () { view; camera.viewport = {width: 100, height: 100}; - camera.bounds = geo.camera.bounds; + camera.bounds = geo.camera.clipbounds.parallel; view = camera.view; camera._rotate(0); @@ -497,18 +462,15 @@ describe('geo.camera', function () { camera._rotate(30 * Math.PI / 180); node.css('transform', geo.camera.css(camera.view)); - expect(node.position().left).toBeLessThan(-18); - expect(node.position().top).toBeLessThan(-18); + assert_position({left: 0, top: -50, right: 136.60, bottom: 86.60}); camera._rotate(-30 * Math.PI / 180); node.css('transform', geo.camera.css(camera.view)); - expect(node.position().left).toBeCloseTo(0, 4); - expect(node.position().top).toBeCloseTo(0, 4); + assert_position({left: 0, top: 0, right: 100, bottom: 100}); camera._rotate(-30 * Math.PI / 180, {x: 50, y: 0}); node.css('transform', geo.camera.css(camera.view)); - expect(node.position().left).toBeLessThan(-10); - expect(node.position().top).toBeLessThan(-40); + assert_position({left: -43.30, top: -25, right: 93.30, bottom: 111.60}); }); describe('World to display', function () { @@ -520,12 +482,7 @@ describe('geo.camera', function () { camera.bounds = {left: 0, right: 100, bottom: 0, top: 100}; node.css('transform', camera.css()); - assert_position({ - left: 0, - right: 100, - top: 0, - bottom: 100 - }); + assert_position({left: 0, top: 0, right: 100, bottom: 100}); }); it('zoom', function () { @@ -537,21 +494,11 @@ describe('geo.camera', function () { camera.zoom(2); node.css('transform', camera.css()); - assert_position({ - left: -50, - top: -50, - right: 150, - bottom: 150 - }); + assert_position({left: 0, top: -100, right: 200, bottom: 100}); camera.zoom(1 / 4); node.css('transform', camera.css()); - assert_position({ - left: 25, - top: 25, - right: 75, - bottom: 75 - }); + assert_position({left: 0, top: 50, right: 50, bottom: 100}); }); it('pan', function () { @@ -563,21 +510,11 @@ describe('geo.camera', function () { camera.pan({x: 50, y: 0}); node.css('transform', camera.css()); - assert_position({ - left: 50, - top: 0, - right: 150, - bottom: 100 - }); + assert_position({left: 50, top: 0, right: 150, bottom: 100}); camera.pan({x: -25, y: 10}); node.css('transform', camera.css()); - assert_position({ - left: 25, - top: -10, - right: 125, - bottom: 90 - }); + assert_position({left: 25, top: -10, right: 125, bottom: 90}); }); it('pan + zoom', function () { var node = make_node(), @@ -589,21 +526,11 @@ describe('geo.camera', function () { camera.zoom(2); camera.pan({x: 10, y: -5}); node.css('transform', camera.css()); - assert_position({ - left: -30, - top: -40, - right: 170, - bottom: 160 - }); + assert_position({left: 20, top: -90, right: 220, bottom: 110}); camera.zoom(0.5); node.css('transform', camera.css()); - assert_position({ - left: 20, - top: 10, - right: 120, - bottom: 110 - }); + assert_position({left: 20, top: 10, right: 120, bottom: 110}); }); }); }); @@ -780,37 +707,6 @@ describe('geo.camera', function () { expect(c.toString()).toEqual(c.ppMatrix(c.transform)); }); - it('Affine transform generator', function () { - var t, v; - - t = geo.camera.affine( - {x: 10, y: 20, z: 30}, - {x: 2, y: 3, z: 4}, - {x: -10, y: -20, z: -30} - ); - - v = vec3.transformMat4( - vec3.create(), - vec3.fromValues(1, 0, 0), - t - ); - expect(v).toEqual(vec3.fromValues(12, 40, 90)); - - v = vec3.transformMat4( - vec3.create(), - vec3.fromValues(0, 1, 0), - t - ); - expect(v).toEqual(vec3.fromValues(10, 43, 90)); - - v = vec3.transformMat4( - vec3.create(), - vec3.fromValues(0, 0, 1), - t - ); - expect(v).toEqual(vec3.fromValues(10, 40, 94)); - }); - it('Unknown transform error', function () { var c = geo.camera(); expect(function () { c.css('not-valid'); }).toThrow(); @@ -821,4 +717,21 @@ describe('geo.camera', function () { expect(function () { c.viewport = {width: 0, height: 100}; }).toThrow(); expect(function () { c.viewport = {width: 100, height: -100}; }).toThrow(); }); + + it('clipbounds', function () { + var c = geo.camera(); + + expect(c.clipbounds.near).toBe(1); + c.clipbounds = {near: 2, far: -2}; + expect(c.clipbounds.near).toBe(2); + expect(function () { c.clipbounds = {near: 2, far: 2}; }).toThrow(); + expect(function () { c.clipbounds = {left: 2, right: 2}; }).toThrow(); + expect(function () { c.clipbounds = {top: 2, bottom: 2}; }).toThrow(); + c.projection = 'perspective'; + expect(c.clipbounds.near).toBe(0.01); + c.clipbounds = {near: 0.1, far: 1000}; + expect(c.clipbounds.near).toBe(0.1); + c.projection = 'parallel'; + expect(c.clipbounds.near).toBe(2); + }); }); diff --git a/tests/cases/pointFeature.js b/tests/cases/pointFeature.js index 90bde8141e..8050be2684 100644 --- a/tests/cases/pointFeature.js +++ b/tests/cases/pointFeature.js @@ -248,8 +248,8 @@ describe('geo.pointFeature', function () { stepAnimationFrame(); var circles = layer.node().find('circle'); expect(circles.length).toBe(19); - expect(circles.eq(0).attr('r')).toBe('5'); - expect(circles.eq(12).attr('r')).toBe('10'); + expect(circles.eq(0).attr('r')).toBeCloseTo('5'); + expect(circles.eq(12).attr('r')).toBeCloseTo('10'); unmockAnimationFrame(); }); });