You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1326 lines
47 KiB
Plaintext

/**
*@namespace
**/
var Util = {
/**Return the bounds for a given set of points, useful as every class uses a
* similar implementation.
*@param {Array<Point>} points - the points collected around the outside of
* the shape.
*@return {Array<Number>} - returns [minX, minY, maxX, maxY] - bounds, where
* all points are in the bounds.
*@author Maxim Georgievskiy <max.kharkov.ua@gmail.com>
**/
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<Number>} - 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<Number>} 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<Number>} innerBounds the inner bounds
* @param {Array<Number>} 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 <a href="http://STACKoverflow.com/questions/1026069/capitalize-first-letter-of-string-in-javascript">http://STACKoverflow.com/questions/1026069/capitalize-first-letter-of-string-in-javascript</a>
**/
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 <alex@scriptoid.com>
**/
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 <alex@scriptoid.com>
**/
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 <alex@scriptoid.com>
**/
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 <alex@scriptoid.com>
*/
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 <alex@scriptoid.com>
**/
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 <alex@scriptoid.com>
*@author Janis Sejans <janis.sejans@towntech.lv>
**/
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 <alex@scriptoid.com>
*@author Maxim Georgievskiy <max.kharkov.ua@gmail.com>
**/
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 <alex@scriptoid.com>
* */
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 <alex@scriptoid.com>
*/
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<Point>} a set of points ordered clockwise.
* @see <a href="http://local.wasp.uwa.edu.au/~pbourke/geometry/insidepoly/">http://local.wasp.uwa.edu.au/~pbourke/geometry/insidepoly/</a> 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<Point>} 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 <alex@scriptoid.com>
**/
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 <alex@scriptoid.com>
**/
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 <alex@scriptoid.com>
**/
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 <alex@scriptoid.com>
**/
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 <alex@scriptoid.com>
*@author Artyom Pokatilov <artyom.pokatilov@gmail.com>
**/
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 <a href="http://en.wikipedia.org/wiki/Sign_function">http://en.wikipedia.org/wiki/Sign_function</a>
*@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 <a href="http://STACKoverflow.com/questions/18082/validate-numbers-in-javascript-isnumeric">http://STACKoverflow.com/questions/18082/validate-numbers-in-javascript-isnumeric</a>
* @author Zack Newsham <zack_newsham@yahoo.co.uk>
* */
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<count;i++){
res += str;
}
return res;
}
/**
* Set selection on target interval inside text DOM element
* @param {HTMLElement} input - DOM element to set selection
* @param {Number} selectionStart - start position of selection
* @param {Number} selectionEnd - end position of selection
* @author Artyom
**/
function setSelectionRange(input, selectionStart, selectionEnd) {
if (input.setSelectionRange) {
input.focus();
input.setSelectionRange(selectionStart, selectionEnd);
}
}
/**A simple class to detect browser and it's version using navigator properties.
* Computing logic can be found here: http://stackoverflow.com/a/2401861/2097494
*
* @this {Browser}
* @constructor
*
* @author Artyom Pokatilov <artyom.pokatilov@gmail.com>
**/
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 <artyom.pokatilov@gmail.com>
*
* Note: <br/>
* 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 <artyom.pokatilov@gmail.com>
*
* Note: <br/>
* Cross-browser solution for native JS.
*/
function removeNodeList(list) {
var i;
var length = list.length;
for (i = 0; i < length; i++) {
removeElement(list[i]);
}
}