/** *@namespace **/ var Util = { /**Return the bounds for a given set of points, useful as every class uses a * similar implementation. *@param {Array} points - the points collected around the outside of * the shape. *@return {Array} - returns [minX, minY, maxX, maxY] - bounds, where * all points are in the bounds. *@author Maxim Georgievskiy **/ getBounds: function (points) { if (!points.length) return null; var minX = points[0].x; var maxX = minX; var minY = points[0].y; var maxY = minY; for (var i = 1; i < points.length; i++) { minX = Math.min(minX, points[i].x); minY = Math.min(minY, points[i].y); maxX = Math.max(maxX, points[i].x); maxY = Math.max(maxY, points[i].y); } return [minX, minY, maxX, maxY]; }, /** * Converts an RGB color value to HSL. Conversion formula * adapted from http://en.wikipedia.org/wiki/HSL_color_space. * Assumes r, g, and b are contained in the set [0, 255] and * returns h, s, and l in the set [0, 1]. * * @param {Number} r - The red color value * @param {Number} g - The green color value * @param {Number} b - The blue color value * @return {Array} - the HSL representation * * Taken from: http://stackoverflow.com/a/9493060 */ rgbToHsl: function (r, g, b) { r /= 255; g /= 255; b /= 255; var max = Math.max(r, g, b); var min = Math.min(r, g, b); var h, s, l = (max + min) / 2; if (max === min) { h = s = 0; // achromatic } else { var d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } return [h, s, l]; }, /** * Converts hex representation of RGB color (#33ffee) to object [r, g, b], * where r, g, b values are contained in the set [0, 255]. * * @param {String} hex - Hex representation of rgb color * @return {Object} - in a form of * { * r - red color value, * g - green color value, * b - blue color value * } * * Taken from: http://stackoverflow.com/a/5624139 */ hexToRgb: function (hex) { // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; hex = hex.replace(shorthandRegex, function (m, r, g, b) { return r + r + g + g + b + b; }); var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null; }, /** * Converts hsl representation array [h,s,l] contained in the set [0,1] * to css-applicable string, like '(14%,80%,75%)' * * @param {Array} hsl - Hsl representation array [h,s,l] * @return {String} - css-applicable string of hsl * @author Arty */ hslToString: function (hsl) { return 'hsl(' + hsl[0] * 360 + ', ' + hsl[1] * 100 + '%, ' + hsl[2] * 100 + '%)'; }, /***/ getUnionBounds: function (shapes) { //tODo }, /** * See if some bounds are inside other bounds. * Bounds are in form [minX, minY, maxX, maxY] * @param {Array} innerBounds the inner bounds * @param {Array} outerBounds the outer bounds * @return {Boolean} true if innerBounds are inside outerBounds * */ areBoundsInBounds: function (innerBounds, outerBounds) { return (outerBounds[0] <= innerBounds[0] && (innerBounds[0] <= outerBounds[2])) && (outerBounds[1] <= innerBounds[1] && (innerBounds[1] <= outerBounds[3])) && (outerBounds[0] <= innerBounds[2] && (innerBounds[2] <= outerBounds[2])) && (outerBounds[1] <= innerBounds[3] && (innerBounds[3] <= outerBounds[3])); }, /**Returns a Polygon out of an Array of points *@param {Array} data - [minX, minY, maxX, maxY] **/ boundsToPolygon: function (data) { var poly = new Polygon(); poly.addPoint(new Point(data[0], data[1])); poly.addPoint(new Point(data[2], data[1])); poly.addPoint(new Point(data[2], data[3])); poly.addPoint(new Point(data[0], data[3])); return poly; }, /**Updates first letter of a string *@param {String} string - the actual string *@return {String} the string with first letter capitalized *@see http://STACKoverflow.com/questions/1026069/capitalize-first-letter-of-string-in-javascript **/ capitaliseFirstLetter: function (string) { return string.charAt(0).toUpperCase() + string.slice(1); }, /**Increase the area of a rectangle by size in any direction *@param {Array} rectangle - the [topX, topY, bottomX, bottomY] *@param {Number} size - the size to increase the rectangle in any direction *@return {Array} - the new reactangle increased :) *@author Alex Gheorghiu **/ feather: function (rectangle, size) { return [rectangle[0] + size, rectangle[1] + size, rectangle[2] + size, rectangle[3] + size]; }, /**Returns the distance between 2 points *@param {Point} p1 - first {Point} *@param {Point} p2 - second {Point} *@return {Number} - the distance between those 2 points. It is always positive. *@author Alex Gheorghiu **/ distance: function (p1, p2) { return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); }, /**Find the location of a point located on segment [p1,p2] at a certain distance from p1 *@param {Point} p1 - first {Point} *@param {Point} p2 - second {Point} *@param {Number} distance_from_p1 - the distance from P1 toward P2 where searched point should be *@return {Point} - the distance between those 2 points. It is always positive. *@author Alex Gheorghiu **/ point_on_segment: function (p1, p2, distance_from_p1) { var d = Util.distance(p1, p2); var Xm = p1.x + distance_from_p1 / d * (p2.x - p1.x); var Ym = p1.y + distance_from_p1 / d * (p2.y - p1.y); return new Point(Xm, Ym); }, /* * Creates a set of dashes/dots/etc along a set of points * points = [p1, p2,p3] * pt = [10,2,2 4,7] /*the pattern 10 dotts, 2 spaces, 2 dots, 7 spaces, etc * * @param {Context} ctx - Canvas' 2D context * @param {Array} points - an {Array} of {Point}s * @param {Array} pattern - an {Array} of {Integer}s that define the pattern. * Pattern is increased/decreased to accomodate the lineWidth * Ex: Scale whole pattern by line width (ex: if lineWidth = 2 then all variables in patter got multiplied by 2) * * @author Alex Gheorghiu */ decorate: function (ctx, points, pattern) { /*Algorithm: *We begin with first segment and start applying the pattern as many time *as we can on it. If we complete the segment then we move to next segment *but we have to keep the rest of the pattern (that were not painted yet) and *apply on the next segment (or segments). And so on. **/ function info(msg) { // console.info(msg); } function group(name) { // console.group(name); } function groupEnd() { // console.groupEnd(); } var t0 = (new Date).getMilliseconds(); /**Scale the pattern up/down to fit the lineWidth*/ var pt = []; for (var i = 0; i < pattern.length; i++) { pt[i] = pattern[i] * ctx.lineWidth; } /** *@param {Point} p - the {Point} **/ function lineTo(p) { ctx.lineTo(p.x, p.y); } /** *@param {Point} p - the {Point} **/ function moveTo(p) { ctx.moveTo(p.x, p.y); } var current_point = points[0]; //path = 0 i = 0; //current point/segment var pt_i = 0; //index position in pattern var pt_left = pt[0]; // spaces or dotts left to paint from current index position in pattern info("current_point" + current_point); //position at the begining moveTo(current_point); while (i < points.length - 1) { //inside [Pi, Pi+1] segment var segment_path = 0; //how much of current segment was painted group("Paint segment " + i); info("i = " + i + " current_point = " + current_point + " pt_i = " + pt_i + " pt_left = " + pt_left + " segment_path = " + segment_path); if (pt_left < 0) { break; } //paint previous/left part of pattern if (pt_left > 0) { group("Paint rest of pattern"); info("Pattern left, pt_left : " + pt_left + " pt_i : " + pt_i); //are we about to cross to another segment? if (pt_left > Util.distance(current_point, points[i + 1])) { //we exceed current segment info("We exceed current segment"); //paint what is left and move to next segment if (pt_i % 2 == 0) { //dots lineTo(points[i + 1]) } else { //spaces moveTo(points[i + 1]) } //store what was left unpainted segment_path += Util.distance(current_point, points[i + 1]); pt_left = pt_left - Util.distance(current_point, points[i + 1]); current_point = points[i + 1]; i++; //move to next segment groupEnd(); //end inner group groupEnd(); //end outer group continue; } else { //still inside segment info("Painting from rest path pt_i = " + pt_i + " current_segment = " + segment_path); var newP = Util.point_on_segment(current_point, points[i + 1], pt_left); //translate on current_point with pt_left from Pi to Pi+1 info("\t newP = " + newP); if (pt_i % 2 == 0) { //dots lineTo(newP) } else { //spaces moveTo(newP) } segment_path += Util.distance(current_point, newP); current_point = newP; pt_left = 0; pt_i = (pt_i + 1) % pt.length; } groupEnd(); } /*We should have: pt_i >= 0 pt_left = 0; */ group('No rest left, normal paint'); info("We should have (pt_i >= 0) and (pt_left = 0) AND WE HAVE " + "pt_i = " + pt_i + " pt_left = " + pt_left); //nothing left from previous segment while (segment_path < Util.distance(points[i], points[i + 1])) { info("Distance between " + i + " and " + (i + 1) + " = " + Util.distance(points[i], points[i + 1])); info("...painting path pt_i = " + pt_i + " dot/space length = " + pt[pt_i] + " current_segment = " + segment_path); if (segment_path + pt[pt_i] <= Util.distance(points[i], points[i + 1])) { //still inside segment group("Still inside segment"); var newP = Util.point_on_segment(current_point, points[i + 1], pt[pt_i]); //translate on current segment with pt[pt_i] from Pi to Pi+1 info("\t newP = " + newP); if (pt_i % 2 == 0) { lineTo(newP); } else { moveTo(newP); } pt_left = 0; segment_path += pt[pt_i]; current_point = newP; pt_i = (pt_i + 1) % pt.length; groupEnd(); } else { //segment exceeded group("Exceed segment"); if (pt_i % 2 == 0) { lineTo(points[i + 1]); } else { moveTo(points[i + 1]); } pt_left = pt[pt_i] - Util.distance(current_point, points[i + 1]); segment_path += Util.distance(current_point, points[i + 1]); current_point = points[i + 1]; info("...pt_left = " + pt_left + " current_segment = " + segment_path + " current_point = " + current_point); //move to next segment groupEnd(); //end inner group break; } } groupEnd(); ++i; groupEnd(); } var t1 = (new Date).getMilliseconds(); console.info("Took " + (t1 - t0) + " ms"); }, /**Trim a number to a fixed number or decimals *@param {Numeric} number - the number to be trimmed *@param {Integer} decimals - the number of decimals to keep *@author Zack **/ round: function (number, decimals) { return Math.round(number * Math.pow(10, decimals)) / Math.pow(10, decimals); }, /**Returns the lenght between 2 points *@param {Point} startPoint - one point *@param {Point} endPoint - the other point *@return {Number} - the distance **/ getLength: function (startPoint, endPoint) { return Math.sqrt(Math.pow(startPoint.x - endPoint.x, 2) + Math.pow(startPoint.y - endPoint.y, 2)); }, /**Returns the middle point between 2 points *@param {Point} startPoint - one point *@param {Point} endPoint - the other point *@return {Point} the middle point **/ getMiddle: function (startPoint, endPoint) { return new Point((startPoint.x + endPoint.x) / 2, (startPoint.y + endPoint.y) / 2); }, /**Returns the length of a Polyline that would be created with a set of points *@param {Array} v - an {Array} of {Points} *@return {Number} - a positive number equal with total length*/ getPolylineLength: function (v) { var l = 0; for (var i = 0; i < v.length - 1; i++) { l += Util.getLength(v[i], v[i + 1]); } return l; }, /** *Tests if a a line defined by 2 points intersects a rectangle *@param {Point} startPoint - the starting point *@param {Point} endPoint - the ending point *@param {Array} bounds - the bounds of the rectangle defined by (x1, y1, x2, y2) *@return true - if line intersects the rectangle, false - if not *@author Alex Gheorghiu **/ lineIntersectsRectangle: function (startPoint, endPoint, bounds) { //create the initial line/segment var l = new Line(startPoint, endPoint); //get the 4 lines/segments represented by the bounds var lines = []; lines.push(new Line(new Point(bounds[0], bounds[1]), new Point(bounds[2], bounds[1]))); lines.push(new Line(new Point(bounds[2], bounds[1]), new Point(bounds[2], bounds[3]))); lines.push(new Line(new Point(bounds[2], bounds[3]), new Point(bounds[0], bounds[3]))); lines.push(new Line(new Point(bounds[0], bounds[3]), new Point(bounds[0], bounds[1]))); //check if our line intersects any of the 4 lines for (var i = 0; i < lines.length; i++) { if (this.lineIntersectsLine(l, lines[i])) { return true; } } return false; }, /** *Tests if a a polyline defined by a set of points intersects a rectangle *@param {Array} points - and {Array} of {Point}s *@param {Array} bounds - the bounds of the rectangle defined by (x1, y1, x2, y2) *@param {Boolean} closedPolyline - incase polyline is closed figure then true, else false * *@return true - if line intersects the rectangle, false - if not *@author Alex Gheorghiu *@author Janis Sejans **/ polylineIntersectsRectangle: function (points, bounds, closedPolyline) { //get the 4 lines/segments represented by the bounds var lines = []; lines.push(new Line(new Point(bounds[0], bounds[1]), new Point(bounds[2], bounds[1]))); lines.push(new Line(new Point(bounds[2], bounds[1]), new Point(bounds[2], bounds[3]))); lines.push(new Line(new Point(bounds[2], bounds[3]), new Point(bounds[0], bounds[3]))); lines.push(new Line(new Point(bounds[0], bounds[3]), new Point(bounds[0], bounds[1]))); for (var k = 0; k < points.length - 1; k++) { //create a line out of each 2 consecutive points var tempLine = new Line(points[k], points[k + 1]); //see if that line intersect any of the line on bounds border for (var i = 0; i < lines.length; i++) { if (this.lineIntersectsLine(tempLine, lines[i])) { return true; } } } //check the closed figure - that is last point connected to the first if (closedPolyline) { //create a line out of each 2 consecutive points var tempLine = new Line(points[points.length - 1], points[0]); //see if that line intersect any of the line on bounds border for (var i = 0; i < lines.length; i++) { if (this.lineIntersectsLine(tempLine, lines[i])) { return true; } } } return false; }, /** *Test to see if 2 {Line}s intersects. They are considered finite segments *and not the infinite lines from geometry *@param {Line} l1 - fist line/segment *@param {Line} l2 - last line/segment *@return {Boolean} true - if the lines intersect or false if not *@author Alex Gheorghiu *@author Maxim Georgievskiy **/ lineIntersectsLine: function (l1, l2) { // check for two vertical lines if (l1.startPoint.x == l1.endPoint.x && l2.startPoint.x == l2.endPoint.x) { return l1.startPoint.x == l2.startPoint.x ? // if 'infinite 'lines do coincide, // then check segment bounds for overlapping l1.contains(l2.startPoint.x, l2.startPoint.y) || l1.contains(l2.endPoint.x, l2.endPoint.y) : // lines are paralel false; } // if one line is vertical, and another line is not vertical else if (l1.startPoint.x == l1.endPoint.x || l2.startPoint.x == l2.endPoint.x) { // let assume l2 is vertical, otherwise exchange them if (l1.startPoint.x == l1.endPoint.x) { var l = l1; l1 = l2; l2 = l; } // finding intersection of 'infinite' lines // equation of the first line is y = ax + b, second: x = c var a = (l1.endPoint.y - l1.startPoint.y) / (l1.endPoint.x - l1.startPoint.x); var b = l1.startPoint.y - a * l1.startPoint.x; var x0 = l2.startPoint.x; var y0 = a * x0 + b; return l1.contains(x0, y0) && l2.contains(x0, y0); } // check normal case - both lines are not vertical else { //line equation is : y = a*x + b, b = y - a * x var a1 = (l1.endPoint.y - l1.startPoint.y) / (l1.endPoint.x - l1.startPoint.x); var b1 = l1.startPoint.y - a1 * l1.startPoint.x; var a2 = (l2.endPoint.y - l2.startPoint.y) / (l2.endPoint.x - l2.startPoint.x); var b2 = l2.startPoint.y - a2 * l2.startPoint.x; if (a1 == a2) { //paralel lines return b1 == b2 ? // for coincide lines, check for segment bounds overlapping l1.contains(l2.startPoint.x, l2.startPoint.y) || l1.contains(l2.endPoint.x, l2.endPoint.y) : // not coincide paralel lines have no chance to intersect false; } else { //usual case - non paralel, the 'infinite' lines intersects...we only need to know if inside the segment /* * if one of the lines are vertical, then x0 is equal to their x, * otherwise: * y1 = a1 * x + b1 * y2 = a2 * x + b2 * => x0 = (b2 - b1) / (a1 - a2) * => y0 = a1 * x0 + b1 **/ x0 = (b2 - b1) / (a1 - a2); y0 = a1 * x0 + b1; return l1.contains(x0, y0) && l2.contains(x0, y0); } } }, /**Tests if 3 points are coliniar (similar to geometry ...with infinite lines) *@param {Point} p1 - first point *@param {Point} p2 - second point *@param {Point} p3 - third point *@return {Boolean} - true if coliniar and false if not *@author Alex *@deprecated **/ deprecated_collinearity: function (p1, p2, p3) { //Check if 2 points coincide. If they do we automatically have collinearity if (p1.x === p2.x && p1.y === p2.y) { return true; } if (p1.x === p3.x && p1.y === p3.y) { return true; } if (p2.x === p3.x && p2.y === p3.y) { return true; } // check for vertical line if (p1.x === p2.x) { return p3.x === p1.x; } else { // usual (not vertical) line can be represented as y = a * x + b var a = (p2.y - p1.y) / (p2.x - p1.x); var b = p1.y - a * p1.x; return p3.y === a * p3.x + b; } }, /**Tests if 3 points are coliniar with matrix determinants. * If the determinat of matrix * / \ * | x1 y1 1 | * | x2 y2 1 | * | x3 y3 1 | * \ / * is zero it means that the points are colinear *@param {Point} p1 - first point *@param {Point} p2 - second point *@param {Point} p3 - third point *@return {Boolean} - true if coliniar and false if not *@author Alex *@see http://en.wikipedia.org/wiki/Determinant *@see https://people.richland.edu/james/lecture/m116/matrices/applications.html **/ collinearity: function (p1, p2, p3, precission) { var determinant = (p1.x * p2.y + p1.y * p3.x + p2.x * p3.y) - (p2.y * p3.x + p1.y * p2.x + p1.x * p3.y); if (precission) { return Math.abs(determinant) <= precission; } else { return determinant === 0; } }, /** It will return the end point of a line on a given angle (clockwise). * @param {Point} startPoint - the start of the line * @param {Number} length - the length of the line * @param {Number} angle - the angle of the line in radians * @return {Point} - the endPoint of the line * @author Zack */ getEndPoint: function (startPoint, length, angle) { var endPoint = startPoint.clone(); endPoint.transform(Matrix.translationMatrix(-startPoint.x, -startPoint.y)); endPoint.y -= length; endPoint.transform(Matrix.rotationMatrix(angle)); endPoint.transform(Matrix.translationMatrix(startPoint.x, startPoint.y)); return endPoint; }, /** Will return the angle of rotation between 2 points, with 0 being north. * Actually the angle with N on a compass * @param {@link Point} centerPoint - the point that is to be considered the center of the shape * @param {@link Point} outsidePoint - the point that we need to find the rotation about the center. * @param {Number} round - amount to round to nearest angle (optional). Think of it as precision * @return {Number} - the angle in radians * @see /documentation/specs/getAngle.png * @author Alex Gheorghiu * */ getAngle: function (centerPoint, outsidePoint, round) { centerPoint.x = Util.round(centerPoint.x, 5); centerPoint.y = Util.round(centerPoint.y, 5); outsidePoint.x = Util.round(outsidePoint.x, 5); outsidePoint.y = Util.round(outsidePoint.y, 5); var angle = Math.atan((outsidePoint.x - centerPoint.x) / (outsidePoint.y - centerPoint.y)); angle = -angle; //endAngle+=90; if (outsidePoint.x >= centerPoint.x && outsidePoint.y >= centerPoint.y) { angle += Math.PI; } else if (outsidePoint.x <= centerPoint.x && outsidePoint.y >= centerPoint.y) { angle += Math.PI; } else if (outsidePoint.x <= centerPoint.x && outsidePoint.y <= centerPoint.y) { angle += Math.PI * 2; } while (angle >= Math.PI * 2) { angle -= Math.PI * 2; } if (isNaN(angle)) {//Nan angle = 0; //we are at center point; } if (round) { angle = Math.round(angle / round) * round } return angle; }, /** *Computes the angle formed by 3 {Point}s *@param {@link Point} startPoint - the start point *@param {@link Point} centerPoint - the center/tid of the angle point *@param {@link Point} endPoint - the end/angle point *@param {Number} round - amount to round to nearest angle (optional) *@author Alex Gheorghiu */ getAngle3Points: function (startPoint, centerPoint, endPoint, round) { var a1 = Util.getAngle(centerPoint, startPoint); var a2 = Util.getAngle(centerPoint, endPoint); var angle = a2 - a1; if (round) { angle = Math.round(angle / round) * round } return angle; }, /** /* Tests whether a point is inside the area (excluding border) determined by a set of other points. * If the points is on the border of the area it will not be counted * * @param point {Point} the point we want to chek * @param points {Array} a set of points ordered clockwise. * @see http://local.wasp.uwa.edu.au/~pbourke/geometry/insidepoly/ solution 1 * */ isPointInside: function (point, points) { if (points.length < 3) { return false; } var counter = 0; var p1 = points[0]; //calulates horizontal intersects for (var i = 1; i <= points.length; i++) { var p2 = points[i % points.length]; if (point.y > Math.min(p1.y, p2.y)) { //our point is between start(Y) and end(Y) points if (point.y <= Math.max(p1.y, p2.y)) { if (point.x <= Math.max(p1.x, p2.x)) { //to the left of any point if (p1.y != p2.y) { //no horizontal line var xinters = (point.y - p1.y) * (p2.x - p1.x) / (p2.y - p1.y) + p1.x; //get slope of line and make it start from the same place as p1 if (p1.x == p2.x || point.x <= xinters) { //if vertical line or our x is before the end x of the actual line. counter++; //we have an intersection } } } } } p1 = p2; } if (counter % 2 == 0) { return false; } else { return true; } }, /** /* Tests whether a point is inside the area (including border) determined by a set of other points. * If the points is on the border of the area it will be counted * * Algorithm: just get border (min/max) values for x and y * and then check if target point inside and on a borer or outside of points * * @param point {Point} the point we want to check * @param points {Array} a set of points ordered clockwise. * */ isPointInsideOrOnBorder: function (point, points) { if (points.length < 3) { return false; } // set min & max values to coordinates of first point var minX = points[0].x; var maxX = points[0].x; var minY = points[0].y; var maxY = points[0].y; // go through points and get min and max x, y values for (var i = 1; i < points.length; i++) { var p = points[i]; minX = Math.min(p.x, minX); maxX = Math.max(p.x, maxX); minY = Math.min(p.y, minY); maxY = Math.max(p.y, maxY); } // check if point is inside and on a border of points or outside if (point.x >= minX && point.x <= maxX && point.y >= minY && point.y <= maxY) { return true; } else { return false; } }, /** * Calculates the number of times the vector from P0(x0,y0) to P1(x1,y1) * crosses the ray extending to the right from P(px,py). * @return {Number} * 0 - if the point lies on the line or no intersection * +1 - if intersection happened and the Y coordinate is increasing (y0 < y1) * -1 - if intersection happened and the Y coordinate is decreasing (y0 >= y1) * @param {Number} px coordinates x for point P * @param {Number} py coordinates y for point P * @param {Number} x0 coordinates x for point P0 * @param {Number} y0 coordinates y for point P0 * @param {Number} x1 coordinates x for point P1 * @param {Number} y1 coordinates y for point P1 * Note: This is pretty much similar to what we have in isPointInside(...) method * but this was inspired from JDK thus older and not used */ pointCrossingsForLine: function (px, py, x0, y0, x1, y1) { if (py < y0 && py < y1) return 0; if (py >= y0 && py >= y1) return 0; if (px >= x0 && px >= x1) return 0; // assert(y0 != y1); if (y0 == y1) throw Exception('Asserted: ' + y0 + ' == ' + y1); if (px < x0 && px < x1) return (y0 < y1) ? 1 : -1; var xintercept = x0 + (py - y0) * (x1 - x0) / (y1 - y0); if (px >= xintercept) return 0; return (y0 < y1) ? 1 : -1; }, /** * Calculates the number of times the cubic from point P0(x0,y0) to point P1(x1,y1) * crosses the ray extending to the right from point P(px,py). * @return {Number} * 0 - If the point lies on a part of the curve or no intersetion * +1 - is added for each crossing where the Y coordinate is increasing * -1 - is added for each crossing where the Y coordinate is decreasing * * @param {Number} px coordinates x for point P * @param {Number} py coordinates y for point P * @param {Number} x0 coordinates x for point P0 (start point) * @param {Number} y0 coordinates y for point P0 (start point) * @param {Number} xc0 coordinates x for point C0 (first controll point) * @param {Number} yc0 coordinates y for point C0 (first controll point) * @param {Number} xc1 coordinates x for point C1 (second controll point) * @param {Number} yc1 coordinates y for point C1 (second controll point) * @param {Number} x1 coordinates x for point P1 (end point) * @param {Number} y1 coordinates y for point P1 (end point) * @param {Number} level The level parameter should be 0 at the top-level * call and will count up for each recursion level to prevent infinite recursion * @see http://www.atalasoft.com/blogs/stevehawley/may-2013/how-to-split-a-cubic-bezier-curve */ pointCrossingsForCubic: function (px, py, x0, y0, xc0, yc0, xc1, yc1, x1, y1, level) { if (py < y0 && py < yc0 && py < yc1 && py < y1) return 0; if (py >= y0 && py >= yc0 && py >= yc1 && py >= y1) return 0; // Note y0 could equal yc0... if (px >= x0 && px >= xc0 && px >= xc1 && px >= x1) return 0; if (px < x0 && px < xc0 && px < xc1 && px < x1) { if (py >= y0) { if (py < y1) return 1; } else { // py < y0 if (py >= y1) return -1; } // py outside of y01 range, and/or y0==yc0 return 0; } // double precision only has 52 bits of mantissa (Give up and fall back to line intersection) if (level > 52) return pointCrossingsForLine(px, py, x0, y0, x1, y1); //"split" current cubic into 2 new cubic curves var xmid = (xc0 + xc1) / 2; var ymid = (yc0 + yc1) / 2; xc0 = (x0 + xc0) / 2; yc0 = (y0 + yc0) / 2; xc1 = (xc1 + x1) / 2; yc1 = (yc1 + y1) / 2; var xc0m = (xc0 + xmid) / 2; var yc0m = (yc0 + ymid) / 2; var xmc1 = (xmid + xc1) / 2; var ymc1 = (ymid + yc1) / 2; xmid = (xc0m + xmc1) / 2; ymid = (yc0m + ymc1) / 2; if (isNaN(xmid) || isNaN(ymid)) { // [xy]mid are NaN if any of [xy]c0m or [xy]mc1 are NaN // [xy]c0m or [xy]mc1 are NaN if any of [xy][c][01] are NaN // These values are also NaN if opposing infinities are added return 0; } return (Util.pointCrossingsForCubic(px, py, x0, y0, xc0, yc0, xc0m, yc0m, xmid, ymid, level + 1) + Util.pointCrossingsForCubic(px, py, xmid, ymid, xmc1, ymc1, xc1, yc1, x1, y1, level + 1)); }, /**Returns the min of a vector *@param {Array} v - vector of {Number}s *@return {Number} - the minimum number from the vector or NaN if vector is empty *@author alex@scriptoid.com **/ min: function (v) { if (v.lenght == 0) { return NaN; } else { var m = v[0]; for (var i = 0; i < v.length; i++) { if (m > v[i]) { m = v[i]; } } return m; } }, /**Returns the max of a vector *@param {Array} v - vector of {Number}s *@return {Number} - the maximum number from the vector or NaN if vector is empty *@author alex@scriptoid.com **/ max: function (v) { if (v.lenght == 0) { return NaN; } else { var m = v[0]; for (var i = 0; i < v.length; i++) { if (m < v[i]) { m = v[i]; } } return m; } }, /** *Tests if a vector of points is a valid path (not going back) *There are a few problems here. If you have p1, p2, p3 and p4 and p2 = p3 you need to ignore that *@param {Array} v - an {Array} of {Point}s *@return {Boolean} - true if path is valid, false otherwise *@author Alex **/ forwardPath: function (v) { if (v.length <= 2) { return true; } for (var i = 0; i < v.length - 2; i++) { if (v[i].x == v[i + 1].x && v[i + 1].x == v[i + 2].x) { //on the same vertical if (signum(v[i + 1].y - v[i].y) != 0) { //test only we have a progressing path if (signum(v[i + 1].y - v[i].y) == -1 * signum(v[i + 2].y - v[i + 1].y)) { //going back (ignore zero) return false; } } } else if (v[i].y == v[i + 1].y && v[i + 1].y == v[i + 2].y) { //on the same horizontal if (signum(v[i + 1].x - v[i].x) != 0) { //test only we have a progressing path if (signum(v[i + 1].x - v[i].x) == -1 * signum(v[i + 2].x - v[i + 1].x)) { //going back (ignore zero) return false; } } } } return true; }, /** *Tests if a vector of points is an orthogonal path (moving in multiples of 90 degrees) *@param {Array} v - an {Array} of {Point}s *@return {Boolean} - true if path is valid, false otherwise *@author Alex **/ orthogonalPath: function (v) { if (v.length <= 1) { return true; } for (var i = 0; i < v.length - 1; i++) { if (v[i].x != v[i + 1].x && v[i].y != v[i + 1].y) { return false; } } return true; }, /**Tries to cut the unecessary poins. *Ex: If you have 3 points A,B and C and they are collinear then B will be cut *@param {Array} v - an {Array} of {Point}s *@return {Array} - the "reduced" vector of {Point}s *@author Alex **/ collinearReduction: function (v) { var r = []; if (v.length < 3) { return Point.cloneArray(v); } r.push(v[0].clone()); for (var i = 1; i < v.length - 1; i++) { if ((v[i - 1].x == v[i].x && v[i].x == v[i + 1].x) || (v[i - 1].y == v[i].y && v[i].y == v[i + 1].y)) { continue; } else { r.push(v[i].clone()); } } r.push(v[v.length - 1].clone()); return r; }, /**Score a ortogonal path made out of Points *Iterates over a set of points (minimum 3) *For each 3 points (i, i+1, i+2) : * - if the 3rd one is after the 2nd on the same line we add +1 * - if the 3rd is up or down related to the 2nd we do not do anything +0 * - if the 3rd goes back we imediatelly return -1 *@param {Array} v - an array of {Point}s *@return {Number} - -1 if the path is wrong (goes back) or something >= 0 if is fine * The bigger the number the smooth the path is *@author Alex Gheorghiu **/ scorePath: function (v) { if (v.length <= 2) { return -1; } var score = 0; for (var i = 1; i < v.length - 1; i++) { if (v[i - 1].x == v[i].x && v[i].x == v[i + 1].x) { //on the same vertical if (signum(v[i + 1].y - v[i].y) == signum(v[i].y - v[i - 1].y)) { //same direction score++; } else { //going back - no good return -1; } } else if (v[i - 1].y == v[i].y && v[i].y == v[i + 1].y) { //on the same horizontal if (signum(v[i + 1].x - v[i].x) == signum(v[i].x - v[i - 1].x)) { //same direction score++; } else { //going back - no good return -1; } } else { //not on same vertical nor horizontal score--; } } return score; }, /** * Function to be used as a replacer in JSON's stringify process. * We need this as Opera does some rounding (@see https://bitbucket.org/scriptoid/diagramo/issue/35/serialization-fails-on-opera-1115) * @param {String} key the property of an object * @param val the value of the key * @see https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/JSON/stringify * @see https://developer.mozilla.org/en-US/docs/Using_native_JSON#The_replacer_parameter * @see https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Number/toFixed * @author Artyom, Alex * @deprecated Since Opera 19.0.1326.47 problem dissapeared. So 0.8999999999999999.toString() no longer returns "0.9" * */ operaReplacer: function (key, val) { if (typeof (val) !== 'undefined' && val !== null) { // As toFixed(...) method is specific only for Number type we will use it to test if val is actually a Number if (val.toFixed) { val = val.toFixed(20); //this will ensure that ANY string representation will have a . (dot) and some 0 (zero)s at the end // check if val has decimals and it ends with zero(s) if (/\.\d*0+$/.test(val)) { // remove last decimal zero(s) from the end of val (and with dot if it is actually) val = val.replace(/(\.)?0+$/, ''); } } } return val; /*by default the return will be undefined which means the 'key' will not be stringified * "If you return undefined, the property is not included in the output JSON string." */ }, /**Creates a new primitive out of JSON parsed object *@param {JSONObject} o - the JSON parsed object of primitive *@return {primitive} a newly constructed primitive *@author Alex Gheorghiu *@author Artyom Pokatilov **/ loadPrimitive: function (o) { var result = null; /**We can not use instanceof Point construction as *the JSON objects are typeless... so JSONObject are simply objects */ if (o.oType == 'Point') { result = Point.load(o); } else if (o.oType == 'Line') { result = Line.load(o); } else if (o.oType == 'Polyline') { result = Polyline.load(o); } else if (o.oType == 'Polygon') { result = Polygon.load(o); } else if (o.oType == 'DottedPolygon') { result = DottedPolygon.load(o); } else if (o.oType == 'QuadCurve') { result = QuadCurve.load(o); } else if (o.oType == 'CubicCurve') { result = CubicCurve.load(o); } else if (o.oType == 'Arc') { result = Arc.load(o); } else if (o.oType == 'Ellipse') { result = Ellipse.load(o); } else if (o.oType == 'DashedArc') { result = DashedArc.load(o); } else if (o.oType == 'Text') { result = Text.load(o); } else if (o.oType == 'Path') { result = Path.load(o); } else if (o.oType == 'Figure') { result = Figure.load(o); //kinda recursevly } else if (o.oType == 'ImageFrame') { result = ImageFrame.load(o); //kinda recursevly } return result; }, /** Selects an object using x and y coordinates: * it can be one of: Figure, Group, Connector, Container or none. * * Note: Connectors are more important than Figures and Figures more important * than Container so Connectors > Figures > Container * @param {Number} x - the x coordinate * @param {Number} y - the y coordinate * @return {Object}- in a form of * { * id - id of object or -1 if none, * type - type of object, the same as oType or '' if none * } * @author Arty */ getObjectByXY: function (x, y) { //find Connector at (x,y) var cId = CONNECTOR_MANAGER.connectorGetByXY(x, y); if (cId != -1) { // found a Connector return { id: cId, type: 'Connector' }; } //find Figure at (x,y) var fId = STACK.figureGetByXY(x, y); if (fId != -1) { // found a Figure var gId = STACK.figureGetById(fId).groupId; if (gId != -1) { // if the Figure belongs to a Group then return that Group return { id: gId, type: 'Group' } } else { // lonely Figure return { id: fId, type: 'Figure' }; } } //find Container at (x,y) var contId = STACK.containerGetByXY(x, y); if (contId !== -1) { // found a Container return { id: contId, type: 'Container' }; } // none of above return { id: -1, type: '' }; }, NewGUID: function () { function S4() { return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); } var newGuid = (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4()); return newGuid.toUpperCase(); } }; /**Returns the sign of a number *@param {Number} x - the number *@returns {Number} *@see http://en.wikipedia.org/wiki/Sign_function *@author alex@scriptoid.com **/ function signum(x){ if(x > 0) return 1; else if(x < 0) return -1; else return 0; } /** Check if a value is numeric * @param {String} input - a numeric value * @see http://STACKoverflow.com/questions/18082/validate-numbers-in-javascript-isnumeric * @author Zack Newsham * */ function isNumeric(input){ return (input - 0) == input && (input.length > 0 || input != ""); } /**Repeats a string for several time and return the concatenated result *@param {String} str - the string *@param {Integer} count - the number of time the string should be repeated *@return {String} **/ function repeat(str, count){ var res = ''; for(var i=0;i **/ function Browser() { var N = navigator.appName.toLowerCase(); var ua = navigator.userAgent.toLowerCase(); var M = ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i); var temp = ua.match(/version\/([\.\d]+)/i); if(M && temp != null) { M[2]= temp[1]; } M= M? [M[1], M[2]]: [N, navigator.appVersion,'-?']; this.webkit = M[0].indexOf("chrome") > -1 || M[0].indexOf("safari") > -1; this.opera = M[0].indexOf("opera") > -1; this.msie = M[0].indexOf("msie") > -1; this.mozilla = M[0].indexOf("firefox") > -1; this.version = M[1]; } /** * Removes DOM element * @param {HTMLElement} element - DOM element to remove * * @author Artyom Pokatilov * * Note:
* Cross-browser solution for native JS. */ function removeElement(element) { element && element.parentNode && element.parentNode.removeChild(element); } /** * Removes NodeList * @param {NodeList} list - NodeList to remove * * @author Artyom Pokatilov * * Note:
* Cross-browser solution for native JS. */ function removeNodeList(list) { var i; var length = list.length; for (i = 0; i < length; i++) { removeElement(list[i]); } }