diff --git a/tests/tests.js b/tests/tests.js index ee080d63..0f348cc3 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -283,6 +283,21 @@ define(function(require) { ok(rect1.contains(rect6), "rect1 should contain rect6"); ok(rect1.contains(rect7), "rect1 should contain rect7"); }); + test("check bearing algorithm", function() { + var point1 = new geometry.point(0,0); + var point2 = new geometry.point(2,0); + var line1 = new geometry.line(point1, point2); + equal(line1.bearing(), "N", "Bearing of line1 should be N"); + var pointLondon = new geometry.point(51.50777816772461, -0.12805555760860443); + var pointParis = new geometry.point(48.856614, 2.3522219000000177); + var pointAmsterdam = new geometry.point(52.326947, 4.741287); + var lineLondonParis = new geometry.line(pointLondon, pointParis); + var lineParisLondon = new geometry.line(pointParis, pointLondon); + var lineLondonAmsterdam = new geometry.line(pointLondon, pointAmsterdam); + equal(lineLondonParis.bearing(), "SE", "Bearing from London to Paris sould be SE"); + equal(lineParisLondon.bearing(), "NW", "Bearing from Paris to London sould be NW"); + equal(lineLondonAmsterdam.bearing(), "E", "Bearing from London to Amsterdam sould be E"); + }); module("utils"); test("check reading an IEEE_754 float from 4 bytes" ,function() { diff --git a/www/js/app.js b/www/js/app.js index add3ae13..19895daf 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -416,9 +416,10 @@ define(function(require) { var distanceFromHereHtml = ""; if (title._geolocation && currentCoordinates) { - // If we know the current position and the title position, we display the distance + // If we know the current position and the title position, we display the distance and cardinal direction var distanceKm = (currentCoordinates.distance(title._geolocation) * 6371 / 60).toFixed(1); - distanceFromHereHtml = " (" + distanceKm + " km)"; + var cardinalDirection = currentCoordinates.bearing(title._geolocation); + distanceFromHereHtml = " (" + distanceKm + " km " + cardinalDirection + ")"; } titleListDivHtml += ">adhereTo:) - adhereToRect: function(r) { - if (r.containsPoint(this)){ - return this; - } - this.x = mmin(mmax(this.x, r.x), r.x + r.width); - this.y = mmin(mmax(this.y, r.y), r.y + r.height); - return this; - }, - // Compute the angle between me and `p` and the x axis. - // (cartesian-to-polar coordinates conversion) - // Return theta angle in degrees. - theta: function(p) { - p = point(p); - // Invert the y-axis. - var y = -(p.y - this.y); - var x = p.x - this.x; - // Makes sure that the comparison with zero takes rounding errors into account. - var PRECISION = 10; - // Note that `atan2` is not defined for `x`, `y` both equal zero. - var rad = (y.toFixed(PRECISION) == 0 && x.toFixed(PRECISION) == 0) ? 0 : atan2(y, x); - - // Correction for III. and IV. quadrant. - if (rad < 0) { - rad = 2*PI + rad; - } - return 180*rad / PI; - }, - // Returns distance between me and point `p`. - distance: function(p) { - return line(this, p).length(); - }, - // Returns a manhattan (taxi-cab) distance between me and point `p`. - manhattanDistance: function(p) { - return abs(p.x - this.x) + abs(p.y - this.y); - }, - // Offset me by the specified amount. - offset: function(dx, dy) { - this.x += dx || 0; - this.y += dy || 0; - return this; - }, - magnitude: function() { - return sqrt((this.x*this.x) + (this.y*this.y)) || 0.01; - }, - update: function(x, y) { - this.x = x || 0; - this.y = y || 0; - return this; - }, - round: function(decimals) { - this.x = decimals ? this.x.toFixed(decimals) : round(this.x); - this.y = decimals ? this.y.toFixed(decimals) : round(this.y); - return this; - }, - // Scale the line segment between (0,0) and me to have a length of len. - normalize: function(len) { - var s = (len || 1) / this.magnitude(); - this.x = s * this.x; - this.y = s * this.y; - return this; - }, - difference: function(p) { - return point(this.x - p.x, this.y - p.y); - }, - // Converts rectangular to polar coordinates. - // An origin can be specified, otherwise it's 0@0. - toPolar: function(o) { - o = (o && point(o)) || point(0,0); - var x = this.x; - var y = this.y; - this.x = sqrt((x-o.x)*(x-o.x) + (y-o.y)*(y-o.y)); // r - this.y = toRad(o.theta(point(x,y))); - return this; - }, - // Rotate point by angle around origin o. - rotate: function(o, angle) { - angle = (angle + 360) % 360; - this.toPolar(o); - this.y += toRad(angle); - var p = point.fromPolar(this.x, this.y, o); - this.x = p.x; - this.y = p.y; - return this; - }, - // Move point on line starting from ref ending at me by - // distance distance. - move: function(ref, distance) { - var theta = toRad(point(ref).theta(this)); - return this.offset(cos(theta) * distance, -sin(theta) * distance); - }, - // Returns change in angle from my previous position (-dx, -dy) to my new position - // relative to ref point. - changeInAngle: function(dx, dy, ref) { - // Revert the translation and measure the change in angle around x-axis. - return point(this).offset(-dx, -dy).theta(ref) - this.theta(ref); - }, - equals: function(p) { - return this.x === p.x && this.y === p.y; - } - }; - // Alternative constructor, from polar coordinates. - // @param {number} r Distance. - // @param {number} angle Angle in radians. - // @param {point} [optional] o Origin. - point.fromPolar = function(r, angle, o) { - o = (o && point(o)) || point(0,0); - var x = abs(r * cos(angle)); - var y = abs(r * sin(angle)); - var deg = normalizeAngle(toDeg(angle)); - - if (deg < 90) y = -y; - else if (deg < 180) { x = -x; y = -y; } - else if (deg < 270) x = -x; - - return point(o.x + x, o.y + y); - }; - - // Create a point with random coordinates that fall into the range `[x1, x2]` and `[y1, y2]`. - point.random = function(x1, x2, y1, y2) { - return point(floor(random() * (x2 - x1 + 1) + x1), floor(random() * (y2 - y1 + 1) + y1)); - }; - - // Line. - // ----- - function line(p1, p2) { - if (!(this instanceof line)) - return new line(p1, p2); - this.start = point(p1); - this.end = point(p2); - } - - line.prototype = { - toString: function() { - return this.start.toString() + ' ' + this.end.toString(); - }, - // @return {double} length of the line - length: function() { - return sqrt(this.squaredLength()); - }, - // @return {integer} length without sqrt - // @note for applications where the exact length is not necessary (e.g. compare only) - squaredLength: function() { - var x0 = this.start.x; - var y0 = this.start.y; - var x1 = this.end.x; - var y1 = this.end.y; - return (x0 -= x1)*x0 + (y0 -= y1)*y0; - }, - // @return {point} my midpoint - midpoint: function() { - return point((this.start.x + this.end.x) / 2, - (this.start.y + this.end.y) / 2); - }, - // @return {point} Point where I'm intersecting l. - // @see Squeak Smalltalk, LineSegment>>intersectionWith: - intersection: function(l) { - var pt1Dir = point(this.end.x - this.start.x, this.end.y - this.start.y); - var pt2Dir = point(l.end.x - l.start.x, l.end.y - l.start.y); - var det = (pt1Dir.x * pt2Dir.y) - (pt1Dir.y * pt2Dir.x); - var deltaPt = point(l.start.x - this.start.x, l.start.y - this.start.y); - var alpha = (deltaPt.x * pt2Dir.y) - (deltaPt.y * pt2Dir.x); - var beta = (deltaPt.x * pt1Dir.y) - (deltaPt.y * pt1Dir.x); - - if (det === 0 || - alpha * det < 0 || - beta * det < 0) { - // No intersection found. - return null; - } - if (det > 0){ - if (alpha > det || beta > det){ - return null; - } - } else { - if (alpha < det || beta < det){ - return null; - } - } - return point(this.start.x + (alpha * pt1Dir.x / det), - this.start.y + (alpha * pt1Dir.y / det)); - } - }; - - // Rectangle. - // ---------- - function rect(x, y, w, h) { - if (!(this instanceof rect)) - return new rect(x, y, w, h); - if (y === undefined) { - y = x.y; - w = x.width; - h = x.height; - x = x.x; - } - if (w === undefined && h === undefined) { - // The rectangle is built from topLeft and bottomRight points - var topLeft = x; - var bottomRight = y; - this.x = topLeft.x; - this.y = bottomRight.y; - this.width = bottomRight.x - topLeft.x; - this.height = topLeft.y - bottomRight.y; - } - else { - this.x = x; - this.y = y; - this.width = w; - this.height = h; - } - } - - rect.prototype = { - toString: function() { - return 'x=' + this.x + ' y=' + this.y + ' w=' + this.width + ' h=' + this.height; - }, - sw : function() { - return point(this.x, this.y); - }, - nw : function() { - return point(this.x, this.y + this.height); - }, - se : function() { - return point(this.x + this.width, this.y); - }, - ne : function() { - return point(this.x + this.width, this.y + this.height); - }, - // TODO : rename all this right/left/top/bottom terms because they are misleading (and wrong) in the Evopedia context : replace with N/S/E/W - origin: function() { - return point(this.x, this.y); - }, - corner: function() { - return point(this.x + this.width, this.y + this.height); - }, - topRight: function() { - return point(this.x + this.width, this.y); - }, - bottomLeft: function() { - return point(this.x, this.y + this.height); - }, - center: function() { - return point(this.x + this.width/2, this.y + this.height/2); - }, - // @return {boolean} true if rectangles intersect - intersect: function(r) { - var myOrigin = this.origin(); - var myCorner = this.corner(); - var rOrigin = r.origin(); - var rCorner = r.corner(); - - if (rCorner.x <= myOrigin.x || - rCorner.y <= myOrigin.y || - rOrigin.x >= myCorner.x || - rOrigin.y >= myCorner.y) return false; - return true; - }, - // @return {string} (left|right|top|bottom) side which is nearest to point - // @see Squeak Smalltalk, Rectangle>>sideNearestTo: - sideNearestToPoint: function(p) { - p = point(p); - var distToLeft = p.x - this.x; - var distToRight = (this.x + this.width) - p.x; - var distToTop = p.y - this.y; - var distToBottom = (this.y + this.height) - p.y; - var closest = distToLeft; - var side = 'left'; - - if (distToRight < closest) { - closest = distToRight; - side = 'right'; - } - if (distToTop < closest) { - closest = distToTop; - side = 'top'; - } - if (distToBottom < closest) { - closest = distToBottom; - side = 'bottom'; - } - return side; - }, - // @return {bool} true if point p is insight me - containsPoint: function(p) { - p = point(p); - if (p.x > this.x && p.x < this.x + this.width && - p.y > this.y && p.y < this.y + this.height) { - return true; - } - return false; - }, - // Algorithm copied from java.awt.Rectangle from OpenJDK - // @return {bool} true if rectangle r is inside me - contains: function(r) { - var nr = r.normalized(); - var W = nr.width; - var H = nr.height; - var X = nr.x; - var Y = nr.y; - var w = this.width; - var h = this.height; - if ((w | h | W | H) < 0) { - // At least one of the dimensions is negative... - return false; - } - // Note: if any dimension is zero, tests below must return false... - var x = this.x; - var y = this.y; - if (X < x || Y < y) { - return false; - } - w += x; - W += X; - if (W <= X) { - // X+W overflowed or W was zero, return false if... - // either original w or W was zero or - // x+w did not overflow or - // the overflowed x+w is smaller than the overflowed X+W - if (w >= x || W > w) return false; - } else { - // X+W did not overflow and W was not zero, return false if... - // original w was zero or - // x+w did not overflow and x+w is smaller than X+W - if (w >= x && W > w) return false; - } - h += y; - H += Y; - if (H <= Y) { - if (h >= y || H > h) return false; - } else { - if (h >= y && H > h) return false; - } - return true; - }, - // @return {point} a point on my boundary nearest to p - // @see Squeak Smalltalk, Rectangle>>pointNearestTo: - pointNearestToPoint: function(p) { - p = point(p); - if (this.containsPoint(p)) { - var side = this.sideNearestToPoint(p); - switch (side){ - case "right": return point(this.x + this.width, p.y); - case "left": return point(this.x, p.y); - case "bottom": return point(p.x, this.y + this.height); - case "top": return point(p.x, this.y); - } - } - return p.adhereToRect(this); - }, - // Find point on my boundary where line starting - // from my center ending in point p intersects me. - // @param {number} angle If angle is specified, intersection with rotated rectangle is computed. - intersectionWithLineFromCenterToPoint: function(p, angle) { - p = point(p); - var center = point(this.x + this.width/2, this.y + this.height/2); - var result; - if (angle) p.rotate(center, angle); - - // (clockwise, starting from the top side) - var sides = [ - line(this.origin(), this.topRight()), - line(this.topRight(), this.corner()), - line(this.corner(), this.bottomLeft()), - line(this.bottomLeft(), this.origin()) - ]; - var connector = line(center, p); - - for (var i = sides.length - 1; i >= 0; --i){ - var intersection = sides[i].intersection(connector); - if (intersection !== null){ - result = intersection; - break; - } - } - if (result && angle) result.rotate(center, -angle); - return result; - }, - // Move and expand me. - // @param r {rectangle} representing deltas - moveAndExpand: function(r) { - this.x += r.x; - this.y += r.y; - this.width += r.width; - this.height += r.height; - return this; - }, - round: function(decimals) { - this.x = decimals ? this.x.toFixed(decimals) : round(this.x); - this.y = decimals ? this.y.toFixed(decimals) : round(this.y); - this.width = decimals ? this.width.toFixed(decimals) : round(this.width); - this.height = decimals ? this.height.toFixed(decimals) : round(this.height); - return this; - }, - // Returns a normalized rectangle; i.e., a rectangle that has a non-negative width and height. - // If width < 0 the function swaps the left and right corners, - // and it swaps the top and bottom corners if height < 0 - // like in http://qt-project.org/doc/qt-4.8/qrectf.html#normalized - normalized: function() { - var newx = this.x; - var newy = this.y; - var newwidth = this.width; - var newheight = this.height; - if (this.width < 0) { - newx = this.x + this.width; - newwidth = - this.width; - } - if (this.height < 0) { - newy = this.y + this.height; - newheight = - this.height; - } - return new rect(newx, newy, newwidth, newheight); - } - }; - - // Ellipse. - // -------- - function ellipse(c, a, b) { - if (!(this instanceof ellipse)) - return new ellipse(c, a, b); - c = point(c); - this.x = c.x; - this.y = c.y; - this.a = a; - this.b = b; - } - - ellipse.prototype = { - toString: function() { - return point(this.x, this.y).toString() + ' ' + this.a + ' ' + this.b; - }, - bbox: function() { - return rect(this.x - this.a, this.y - this.b, 2*this.a, 2*this.b); - }, - // Find point on me where line from my center to - // point p intersects my boundary. - // @param {number} angle If angle is specified, intersection with rotated ellipse is computed. - intersectionWithLineFromCenterToPoint: function(p, angle) { - p = point(p); - if (angle) p.rotate(point(this.x, this.y), angle); - var dx = p.x - this.x; - var dy = p.y - this.y; - var result; - if (dx === 0) { - result = this.bbox().pointNearestToPoint(p); - if (angle) return result.rotate(point(this.x, this.y), -angle); - return result; - } - var m = dy / dx; - var mSquared = m * m; - var aSquared = this.a * this.a; - var bSquared = this.b * this.b; - var x = sqrt(1 / ((1 / aSquared) + (mSquared / bSquared))); - - x = dx < 0 ? -x : x; - var y = m * x; - result = point(this.x + x, this.y + y); - if (angle) return result.rotate(point(this.x, this.y), -angle); - return result; - } - }; - - // Bezier curve. - // ------------- - var bezier = { - // Cubic Bezier curve path through points. - // Ported from C# implementation by Oleg V. Polikarpotchkin and Peter Lee (http://www.codeproject.com/KB/graphics/BezierSpline.aspx). - // @param {array} points Array of points through which the smooth line will go. - // @return {array} SVG Path commands as an array - curveThroughPoints: function(points) { - var controlPoints = this.getCurveControlPoints(points); - var path = ['M', points[0].x, points[0].y]; - - for (var i = 0; i < controlPoints[0].length; i++) { - path.push('C', controlPoints[0][i].x, controlPoints[0][i].y, controlPoints[1][i].x, controlPoints[1][i].y, points[i+1].x, points[i+1].y); - } - return path; - }, - - // Get open-ended Bezier Spline Control Points. - // @param knots Input Knot Bezier spline points (At least two points!). - // @param firstControlPoints Output First Control points. Array of knots.length - 1 length. - // @param secondControlPoints Output Second Control points. Array of knots.length - 1 length. - getCurveControlPoints: function(knots) { - var firstControlPoints = []; - var secondControlPoints = []; - var n = knots.length - 1; - var i; - - // Special case: Bezier curve should be a straight line. - if (n == 1) { - // 3P1 = 2P0 + P3 - firstControlPoints[0] = point((2 * knots[0].x + knots[1].x) / 3, - (2 * knots[0].y + knots[1].y) / 3); - // P2 = 2P1 – P0 - secondControlPoints[0] = point(2 * firstControlPoints[0].x - knots[0].x, - 2 * firstControlPoints[0].y - knots[0].y); - return [firstControlPoints, secondControlPoints]; - } - - // Calculate first Bezier control points. - // Right hand side vector. - var rhs = []; - - // Set right hand side X values. - for (i = 1; i < n - 1; i++) { - rhs[i] = 4 * knots[i].x + 2 * knots[i + 1].x; - } - rhs[0] = knots[0].x + 2 * knots[1].x; - rhs[n - 1] = (8 * knots[n - 1].x + knots[n].x) / 2.0; - // Get first control points X-values. - var x = this.getFirstControlPoints(rhs); - - // Set right hand side Y values. - for (i = 1; i < n - 1; ++i) { - rhs[i] = 4 * knots[i].y + 2 * knots[i + 1].y; - } - rhs[0] = knots[0].y + 2 * knots[1].y; - rhs[n - 1] = (8 * knots[n - 1].y + knots[n].y) / 2.0; - // Get first control points Y-values. - var y = this.getFirstControlPoints(rhs); - - // Fill output arrays. - for (i = 0; i < n; i++) { - // First control point. - firstControlPoints.push(point(x[i], y[i])); - // Second control point. - if (i < n - 1) { - secondControlPoints.push(point(2 * knots [i + 1].x - x[i + 1], - 2 * knots[i + 1].y - y[i + 1])); - } else { - secondControlPoints.push(point((knots[n].x + x[n - 1]) / 2, - (knots[n].y + y[n - 1]) / 2)); - } - } - return [firstControlPoints, secondControlPoints]; - }, - - // Solves a tridiagonal system for one of coordinates (x or y) of first Bezier control points. - // @param rhs Right hand side vector. - // @return Solution vector. - getFirstControlPoints: function(rhs) { - var n = rhs.length; - // `x` is a solution vector. - var x = []; - var tmp = []; - var b = 2.0; - - x[0] = rhs[0] / b; - // Decomposition and forward substitution. - for (var i = 1; i < n; i++) { - tmp[i] = 1 / b; - b = (i < n - 1 ? 4.0 : 3.5) - tmp[i]; - x[i] = (rhs[i] - x[i - 1]) / b; - } - for (i = 1; i < n; i++) { - // Backsubstitution. - x[n - i - 1] -= tmp[n - i] * x[n - i]; - } - return x; - } - }; - - return { - - toDeg: toDeg, - toRad: toRad, - snapToGrid: snapToGrid, - normalizeAngle: normalizeAngle, - point: point, - line: line, - rect: rect, - ellipse: ellipse, - bezier: bezier - }; -}); +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Geometry library. +// (c) 2011-2013 client IO +// Copied from https://github.com/DavidDurman/joint/blob/master/src/geometry.js + +define(function(require) { + + + + // Declare shorthands to the most used math functions. + var math = Math; + var abs = math.abs; + var cos = math.cos; + var sin = math.sin; + var sqrt = math.sqrt; + var mmin = math.min; + var mmax = math.max; + var atan = math.atan; + var atan2 = math.atan2; + var acos = math.acos; + var round = math.round; + var floor = math.floor; + var PI = math.PI; + var random = math.random; + var toDeg = function(rad) { return (180*rad / PI) % 360; }; + var toRad = function(deg) { return (deg % 360) * PI / 180; }; + var snapToGrid = function(val, gridSize) { return gridSize * Math.round(val/gridSize); }; + var normalizeAngle = function(angle) { return (angle % 360) + (angle < 0 ? 360 : 0); }; + + // Point + // ----- + + // Point is the most basic object consisting of x/y coordinate,. + + // Possible instantiations are: + + // * `point(10, 20)` + // * `new point(10, 20)` + // * `point('10 20')` + // * `point(point(10, 20))` + function point(x, y) { + if (!(this instanceof point)) + return new point(x, y); + var xy; + if (y === undefined && Object(x) !== x) { + xy = x.split(_.indexOf(x, "@") === -1 ? " " : "@"); + this.x = parseInt(xy[0], 10); + this.y = parseInt(xy[1], 10); + } else if (Object(x) === x) { + this.x = x.x; + this.y = x.y; + } else { + this.x = x; + this.y = y; + } + } + + point.prototype = { + toString: function() { + return this.x + "@" + this.y; + }, + // If point lies outside rectangle `r`, return the nearest point on the boundary of rect `r`, + // otherwise return point itself. + // (see Squeak Smalltalk, Point>>adhereTo:) + adhereToRect: function(r) { + if (r.containsPoint(this)){ + return this; + } + this.x = mmin(mmax(this.x, r.x), r.x + r.width); + this.y = mmin(mmax(this.y, r.y), r.y + r.height); + return this; + }, + // Compute the angle between me and `p` and the x axis. + // (cartesian-to-polar coordinates conversion) + // Return theta angle in degrees. + theta: function(p) { + p = point(p); + // Invert the y-axis. + var y = -(p.y - this.y); + var x = p.x - this.x; + // Makes sure that the comparison with zero takes rounding errors into account. + var PRECISION = 10; + // Note that `atan2` is not defined for `x`, `y` both equal zero. + var rad = (y.toFixed(PRECISION) == 0 && x.toFixed(PRECISION) == 0) ? 0 : atan2(y, x); + + // Correction for III. and IV. quadrant. + if (rad < 0) { + rad = 2*PI + rad; + } + return 180*rad / PI; + }, + // Returns distance between me and point `p`. + distance: function(p) { + return line(this, p).length(); + }, + // Returns the bearing between me and point 'p' + bearing: function(p) { + return line(this, p).bearing(); + }, + // Returns a manhattan (taxi-cab) distance between me and point `p`. + manhattanDistance: function(p) { + return abs(p.x - this.x) + abs(p.y - this.y); + }, + // Offset me by the specified amount. + offset: function(dx, dy) { + this.x += dx || 0; + this.y += dy || 0; + return this; + }, + magnitude: function() { + return sqrt((this.x*this.x) + (this.y*this.y)) || 0.01; + }, + update: function(x, y) { + this.x = x || 0; + this.y = y || 0; + return this; + }, + round: function(decimals) { + this.x = decimals ? this.x.toFixed(decimals) : round(this.x); + this.y = decimals ? this.y.toFixed(decimals) : round(this.y); + return this; + }, + // Scale the line segment between (0,0) and me to have a length of len. + normalize: function(len) { + var s = (len || 1) / this.magnitude(); + this.x = s * this.x; + this.y = s * this.y; + return this; + }, + difference: function(p) { + return point(this.x - p.x, this.y - p.y); + }, + // Converts rectangular to polar coordinates. + // An origin can be specified, otherwise it's 0@0. + toPolar: function(o) { + o = (o && point(o)) || point(0,0); + var x = this.x; + var y = this.y; + this.x = sqrt((x-o.x)*(x-o.x) + (y-o.y)*(y-o.y)); // r + this.y = toRad(o.theta(point(x,y))); + return this; + }, + // Rotate point by angle around origin o. + rotate: function(o, angle) { + angle = (angle + 360) % 360; + this.toPolar(o); + this.y += toRad(angle); + var p = point.fromPolar(this.x, this.y, o); + this.x = p.x; + this.y = p.y; + return this; + }, + // Move point on line starting from ref ending at me by + // distance distance. + move: function(ref, distance) { + var theta = toRad(point(ref).theta(this)); + return this.offset(cos(theta) * distance, -sin(theta) * distance); + }, + // Returns change in angle from my previous position (-dx, -dy) to my new position + // relative to ref point. + changeInAngle: function(dx, dy, ref) { + // Revert the translation and measure the change in angle around x-axis. + return point(this).offset(-dx, -dy).theta(ref) - this.theta(ref); + }, + equals: function(p) { + return this.x === p.x && this.y === p.y; + } + }; + // Alternative constructor, from polar coordinates. + // @param {number} r Distance. + // @param {number} angle Angle in radians. + // @param {point} [optional] o Origin. + point.fromPolar = function(r, angle, o) { + o = (o && point(o)) || point(0,0); + var x = abs(r * cos(angle)); + var y = abs(r * sin(angle)); + var deg = normalizeAngle(toDeg(angle)); + + if (deg < 90) y = -y; + else if (deg < 180) { x = -x; y = -y; } + else if (deg < 270) x = -x; + + return point(o.x + x, o.y + y); + }; + + // Create a point with random coordinates that fall into the range `[x1, x2]` and `[y1, y2]`. + point.random = function(x1, x2, y1, y2) { + return point(floor(random() * (x2 - x1 + 1) + x1), floor(random() * (y2 - y1 + 1) + y1)); + }; + + // Line. + // ----- + function line(p1, p2) { + if (!(this instanceof line)) + return new line(p1, p2); + this.start = point(p1); + this.end = point(p2); + } + + line.prototype = { + toString: function() { + return this.start.toString() + ' ' + this.end.toString(); + }, + // @return {double} length of the line + length: function() { + return sqrt(this.squaredLength()); + }, + // @return {integer} length without sqrt + // @note for applications where the exact length is not necessary (e.g. compare only) + squaredLength: function() { + var x0 = this.start.x; + var y0 = this.start.y; + var x1 = this.end.x; + var y1 = this.end.y; + return (x0 -= x1)*x0 + (y0 -= y1)*y0; + }, + // @return {point} my midpoint + midpoint: function() { + return point((this.start.x + this.end.x) / 2, + (this.start.y + this.end.y) / 2); + }, + // @return {point} Point where I'm intersecting l. + // @see Squeak Smalltalk, LineSegment>>intersectionWith: + intersection: function(l) { + var pt1Dir = point(this.end.x - this.start.x, this.end.y - this.start.y); + var pt2Dir = point(l.end.x - l.start.x, l.end.y - l.start.y); + var det = (pt1Dir.x * pt2Dir.y) - (pt1Dir.y * pt2Dir.x); + var deltaPt = point(l.start.x - this.start.x, l.start.y - this.start.y); + var alpha = (deltaPt.x * pt2Dir.y) - (deltaPt.y * pt2Dir.x); + var beta = (deltaPt.x * pt1Dir.y) - (deltaPt.y * pt1Dir.x); + + if (det === 0 || + alpha * det < 0 || + beta * det < 0) { + // No intersection found. + return null; + } + if (det > 0){ + if (alpha > det || beta > det){ + return null; + } + } else { + if (alpha < det || beta < det){ + return null; + } + } + return point(this.start.x + (alpha * pt1Dir.x / det), + this.start.y + (alpha * pt1Dir.y / det)); + }, + /** + * Returns the bearing (cardinal direction) of the line. For example N, W, or SE + * @returns {String} One of the following bearings : NE, E, SE, S, SW, W, NW, N + */ + bearing: function() { + var lat1 = this.start.x * Math.PI / 180; + var lat2 = this.end.x * Math.PI / 180; + var lon1 = this.start.y * Math.PI / 180; + var lon2 = this.end.y * Math.PI / 180; + var dLon = lon2 - lon1; + var y = Math.sin(dLon) * Math.cos(lat2); + var x = Math.cos(lat1) * Math.sin(lat2) - + Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLon); + var brng = Math.atan2(y, x) / Math.PI * 180; + + var bearings = ["NE", "E", "SE", "S", "SW", "W", "NW", "N"]; + + var index = brng - 22.5; + if (index < 0) + index += 360; + index = parseInt(index / 45); + + return(bearings[index]); + + } + }; + + // Rectangle. + // ---------- + function rect(x, y, w, h) { + if (!(this instanceof rect)) + return new rect(x, y, w, h); + if (y === undefined) { + y = x.y; + w = x.width; + h = x.height; + x = x.x; + } + if (w === undefined && h === undefined) { + // The rectangle is built from topLeft and bottomRight points + var topLeft = x; + var bottomRight = y; + this.x = topLeft.x; + this.y = bottomRight.y; + this.width = bottomRight.x - topLeft.x; + this.height = topLeft.y - bottomRight.y; + } + else { + this.x = x; + this.y = y; + this.width = w; + this.height = h; + } + } + + rect.prototype = { + toString: function() { + return 'x=' + this.x + ' y=' + this.y + ' w=' + this.width + ' h=' + this.height; + }, + sw : function() { + return point(this.x, this.y); + }, + nw : function() { + return point(this.x, this.y + this.height); + }, + se : function() { + return point(this.x + this.width, this.y); + }, + ne : function() { + return point(this.x + this.width, this.y + this.height); + }, + // TODO : rename all this right/left/top/bottom terms because they are misleading (and wrong) in the Evopedia context : replace with N/S/E/W + origin: function() { + return point(this.x, this.y); + }, + corner: function() { + return point(this.x + this.width, this.y + this.height); + }, + topRight: function() { + return point(this.x + this.width, this.y); + }, + bottomLeft: function() { + return point(this.x, this.y + this.height); + }, + center: function() { + return point(this.x + this.width/2, this.y + this.height/2); + }, + // @return {boolean} true if rectangles intersect + intersect: function(r) { + var myOrigin = this.origin(); + var myCorner = this.corner(); + var rOrigin = r.origin(); + var rCorner = r.corner(); + + if (rCorner.x <= myOrigin.x || + rCorner.y <= myOrigin.y || + rOrigin.x >= myCorner.x || + rOrigin.y >= myCorner.y) return false; + return true; + }, + // @return {string} (left|right|top|bottom) side which is nearest to point + // @see Squeak Smalltalk, Rectangle>>sideNearestTo: + sideNearestToPoint: function(p) { + p = point(p); + var distToLeft = p.x - this.x; + var distToRight = (this.x + this.width) - p.x; + var distToTop = p.y - this.y; + var distToBottom = (this.y + this.height) - p.y; + var closest = distToLeft; + var side = 'left'; + + if (distToRight < closest) { + closest = distToRight; + side = 'right'; + } + if (distToTop < closest) { + closest = distToTop; + side = 'top'; + } + if (distToBottom < closest) { + closest = distToBottom; + side = 'bottom'; + } + return side; + }, + // @return {bool} true if point p is insight me + containsPoint: function(p) { + p = point(p); + if (p.x > this.x && p.x < this.x + this.width && + p.y > this.y && p.y < this.y + this.height) { + return true; + } + return false; + }, + // Algorithm copied from java.awt.Rectangle from OpenJDK + // @return {bool} true if rectangle r is inside me + contains: function(r) { + var nr = r.normalized(); + var W = nr.width; + var H = nr.height; + var X = nr.x; + var Y = nr.y; + var w = this.width; + var h = this.height; + if ((w | h | W | H) < 0) { + // At least one of the dimensions is negative... + return false; + } + // Note: if any dimension is zero, tests below must return false... + var x = this.x; + var y = this.y; + if (X < x || Y < y) { + return false; + } + w += x; + W += X; + if (W <= X) { + // X+W overflowed or W was zero, return false if... + // either original w or W was zero or + // x+w did not overflow or + // the overflowed x+w is smaller than the overflowed X+W + if (w >= x || W > w) return false; + } else { + // X+W did not overflow and W was not zero, return false if... + // original w was zero or + // x+w did not overflow and x+w is smaller than X+W + if (w >= x && W > w) return false; + } + h += y; + H += Y; + if (H <= Y) { + if (h >= y || H > h) return false; + } else { + if (h >= y && H > h) return false; + } + return true; + }, + // @return {point} a point on my boundary nearest to p + // @see Squeak Smalltalk, Rectangle>>pointNearestTo: + pointNearestToPoint: function(p) { + p = point(p); + if (this.containsPoint(p)) { + var side = this.sideNearestToPoint(p); + switch (side){ + case "right": return point(this.x + this.width, p.y); + case "left": return point(this.x, p.y); + case "bottom": return point(p.x, this.y + this.height); + case "top": return point(p.x, this.y); + } + } + return p.adhereToRect(this); + }, + // Find point on my boundary where line starting + // from my center ending in point p intersects me. + // @param {number} angle If angle is specified, intersection with rotated rectangle is computed. + intersectionWithLineFromCenterToPoint: function(p, angle) { + p = point(p); + var center = point(this.x + this.width/2, this.y + this.height/2); + var result; + if (angle) p.rotate(center, angle); + + // (clockwise, starting from the top side) + var sides = [ + line(this.origin(), this.topRight()), + line(this.topRight(), this.corner()), + line(this.corner(), this.bottomLeft()), + line(this.bottomLeft(), this.origin()) + ]; + var connector = line(center, p); + + for (var i = sides.length - 1; i >= 0; --i){ + var intersection = sides[i].intersection(connector); + if (intersection !== null){ + result = intersection; + break; + } + } + if (result && angle) result.rotate(center, -angle); + return result; + }, + // Move and expand me. + // @param r {rectangle} representing deltas + moveAndExpand: function(r) { + this.x += r.x; + this.y += r.y; + this.width += r.width; + this.height += r.height; + return this; + }, + round: function(decimals) { + this.x = decimals ? this.x.toFixed(decimals) : round(this.x); + this.y = decimals ? this.y.toFixed(decimals) : round(this.y); + this.width = decimals ? this.width.toFixed(decimals) : round(this.width); + this.height = decimals ? this.height.toFixed(decimals) : round(this.height); + return this; + }, + // Returns a normalized rectangle; i.e., a rectangle that has a non-negative width and height. + // If width < 0 the function swaps the left and right corners, + // and it swaps the top and bottom corners if height < 0 + // like in http://qt-project.org/doc/qt-4.8/qrectf.html#normalized + normalized: function() { + var newx = this.x; + var newy = this.y; + var newwidth = this.width; + var newheight = this.height; + if (this.width < 0) { + newx = this.x + this.width; + newwidth = - this.width; + } + if (this.height < 0) { + newy = this.y + this.height; + newheight = - this.height; + } + return new rect(newx, newy, newwidth, newheight); + } + }; + + // Ellipse. + // -------- + function ellipse(c, a, b) { + if (!(this instanceof ellipse)) + return new ellipse(c, a, b); + c = point(c); + this.x = c.x; + this.y = c.y; + this.a = a; + this.b = b; + } + + ellipse.prototype = { + toString: function() { + return point(this.x, this.y).toString() + ' ' + this.a + ' ' + this.b; + }, + bbox: function() { + return rect(this.x - this.a, this.y - this.b, 2*this.a, 2*this.b); + }, + // Find point on me where line from my center to + // point p intersects my boundary. + // @param {number} angle If angle is specified, intersection with rotated ellipse is computed. + intersectionWithLineFromCenterToPoint: function(p, angle) { + p = point(p); + if (angle) p.rotate(point(this.x, this.y), angle); + var dx = p.x - this.x; + var dy = p.y - this.y; + var result; + if (dx === 0) { + result = this.bbox().pointNearestToPoint(p); + if (angle) return result.rotate(point(this.x, this.y), -angle); + return result; + } + var m = dy / dx; + var mSquared = m * m; + var aSquared = this.a * this.a; + var bSquared = this.b * this.b; + var x = sqrt(1 / ((1 / aSquared) + (mSquared / bSquared))); + + x = dx < 0 ? -x : x; + var y = m * x; + result = point(this.x + x, this.y + y); + if (angle) return result.rotate(point(this.x, this.y), -angle); + return result; + } + }; + + // Bezier curve. + // ------------- + var bezier = { + // Cubic Bezier curve path through points. + // Ported from C# implementation by Oleg V. Polikarpotchkin and Peter Lee (http://www.codeproject.com/KB/graphics/BezierSpline.aspx). + // @param {array} points Array of points through which the smooth line will go. + // @return {array} SVG Path commands as an array + curveThroughPoints: function(points) { + var controlPoints = this.getCurveControlPoints(points); + var path = ['M', points[0].x, points[0].y]; + + for (var i = 0; i < controlPoints[0].length; i++) { + path.push('C', controlPoints[0][i].x, controlPoints[0][i].y, controlPoints[1][i].x, controlPoints[1][i].y, points[i+1].x, points[i+1].y); + } + return path; + }, + + // Get open-ended Bezier Spline Control Points. + // @param knots Input Knot Bezier spline points (At least two points!). + // @param firstControlPoints Output First Control points. Array of knots.length - 1 length. + // @param secondControlPoints Output Second Control points. Array of knots.length - 1 length. + getCurveControlPoints: function(knots) { + var firstControlPoints = []; + var secondControlPoints = []; + var n = knots.length - 1; + var i; + + // Special case: Bezier curve should be a straight line. + if (n == 1) { + // 3P1 = 2P0 + P3 + firstControlPoints[0] = point((2 * knots[0].x + knots[1].x) / 3, + (2 * knots[0].y + knots[1].y) / 3); + // P2 = 2P1 – P0 + secondControlPoints[0] = point(2 * firstControlPoints[0].x - knots[0].x, + 2 * firstControlPoints[0].y - knots[0].y); + return [firstControlPoints, secondControlPoints]; + } + + // Calculate first Bezier control points. + // Right hand side vector. + var rhs = []; + + // Set right hand side X values. + for (i = 1; i < n - 1; i++) { + rhs[i] = 4 * knots[i].x + 2 * knots[i + 1].x; + } + rhs[0] = knots[0].x + 2 * knots[1].x; + rhs[n - 1] = (8 * knots[n - 1].x + knots[n].x) / 2.0; + // Get first control points X-values. + var x = this.getFirstControlPoints(rhs); + + // Set right hand side Y values. + for (i = 1; i < n - 1; ++i) { + rhs[i] = 4 * knots[i].y + 2 * knots[i + 1].y; + } + rhs[0] = knots[0].y + 2 * knots[1].y; + rhs[n - 1] = (8 * knots[n - 1].y + knots[n].y) / 2.0; + // Get first control points Y-values. + var y = this.getFirstControlPoints(rhs); + + // Fill output arrays. + for (i = 0; i < n; i++) { + // First control point. + firstControlPoints.push(point(x[i], y[i])); + // Second control point. + if (i < n - 1) { + secondControlPoints.push(point(2 * knots [i + 1].x - x[i + 1], + 2 * knots[i + 1].y - y[i + 1])); + } else { + secondControlPoints.push(point((knots[n].x + x[n - 1]) / 2, + (knots[n].y + y[n - 1]) / 2)); + } + } + return [firstControlPoints, secondControlPoints]; + }, + + // Solves a tridiagonal system for one of coordinates (x or y) of first Bezier control points. + // @param rhs Right hand side vector. + // @return Solution vector. + getFirstControlPoints: function(rhs) { + var n = rhs.length; + // `x` is a solution vector. + var x = []; + var tmp = []; + var b = 2.0; + + x[0] = rhs[0] / b; + // Decomposition and forward substitution. + for (var i = 1; i < n; i++) { + tmp[i] = 1 / b; + b = (i < n - 1 ? 4.0 : 3.5) - tmp[i]; + x[i] = (rhs[i] - x[i - 1]) / b; + } + for (i = 1; i < n; i++) { + // Backsubstitution. + x[n - i - 1] -= tmp[n - i] * x[n - i]; + } + return x; + } + }; + + return { + + toDeg: toDeg, + toRad: toRad, + snapToGrid: snapToGrid, + normalizeAngle: normalizeAngle, + point: point, + line: line, + rect: rect, + ellipse: ellipse, + bezier: bezier + }; +});