/*
 * L.LineUtil contains different utility functions for line segments 
 * and polylines (clipping, simplification, distances, etc.)
 */

L.LineUtil = {
	/*
	 * Simplify polyline with vertex reduction and Douglas-Peucker simplification.
	 * Improves rendering performance dramatically by lessening the number of points to draw.
	 */
	simplify: function(/*Point[]*/ points, /*Number*/ tolerance) {
		if (!tolerance) return points.slice();
		
		// stage 1: vertex reduction
		points = this.reducePoints(points, tolerance);
		
		// stage 2: Douglas-Peucker simplification
		points = this.simplifyDP(points, tolerance);
		
		return points; 
	},
	
	// distance from a point to a segment between two points
	pointToSegmentDistance:  function(/*Point*/ p, /*Point*/ p1, /*Point*/ p2) {
		return Math.sqrt(this._sqPointToSegmentDist(p, p1, p2));	
	},
	
	// Douglas-Peucker simplification, see http://en.wikipedia.org/wiki/Douglas-Peucker_algorithm
	simplifyDP: function(points, tol) {
		var maxDist2 = 0,
			index = 0,
			t2 = tol * tol;

		for (var i = 1, len = points.length, dist2; i < len - 1; i++) {
			dist2 = this._sqPointToSegmentDist(points[i], points[0], points[len - 1]);
			if (dist2 > maxDist2) {
				index = i;
				maxDist2 = dist2;
			}
		}
		
		if (maxDist2 >= t2) {
			var part1 = points.slice(0, index),
				part2 = points.slice(index),
				simplifiedPart1 = this.simplifyDP(part1, tol).slice(0, len - 2),
				simplifiedPart2 = this.simplifyDP(part2, tol);
			
			return simplifiedPart1.concat(simplifiedPart2);
		} else {
			return [points[0], points[len - 1]];
		}
	},
	
	// reduce points that are too close to each other to a single point
	reducePoints: function(points, tol) {
		var reducedPoints = [points[0]],
			t2 = tol * tol;
		
		for (var i = 1, prev = 0, len = points.length; i < len; i++) {
			if (this._sqDist(points[i], points[prev]) < t2) continue;
			reducedPoints.push(points[i]);
			prev = i;
		}
		if (prev < len - 1) {
			reducedPoints.push(points[len - 1]);
		}
		return reducedPoints;
	},
	
	/*
	 * Cohen-Sutherland line clipping algorithm.
	 * Used to avoid rendering parts of a polyline that are not currently visible.
	 */
	clipSegment: function(a, b, bounds, useLastCode) {
		var min = bounds.min,
			max = bounds.max;
		
		var codeA = useLastCode ? this._lastCode : this._getBitCode(a, bounds),
			codeB = this._getBitCode(b, bounds);
		
		// save 2nd code to avoid calculating it on the next segment
		this._lastCode = codeB;
		
		while (true) {
			// if a,b is inside the clip window (trivial accept)
			if (!(codeA | codeB)) {
				return [a, b];
			// if a,b is outside the clip window (trivial reject)
			} else if (codeA & codeB) {
				return false;
			// other cases
			} else {
				var codeOut = codeA || codeB,
					p = this._getEdgeIntersection(a, b, codeOut, bounds),
					newCode = this._getBitCode(p, bounds);
				
				if (codeOut == codeA) {
					a = p;
					codeA = newCode;
				} else {
					b = p;
					codeB = newCode;
				}
			}
		}
	},
	
	_getEdgeIntersection: function(a, b, code, bounds) {
		var dx = b.x - a.x,
			dy = b.y - a.y,
			min = bounds.min,
			max = bounds.max;
		
		if (code & 8) { // top
			return new L.Point(a.x + dx * (max.y - a.y) / dy, max.y);
		} else if (code & 4) { // bottom
			return new L.Point(a.x + dx * (min.y - a.y) / dy, min.y);
		} else if (code & 2){ // right
			return new L.Point(max.x, a.y + dy * (max.x - a.x) / dx);
		} else if (code & 1) { // left
			return new L.Point(min.x, a.y + dy * (min.x - a.x) / dx);
		}
	},
	
	_getBitCode: function(/*Point*/ p, bounds) {
		var code = 0;
		
		if (p.x < bounds.min.x) code |= 1; // left
		else if (p.x > bounds.max.x) code |= 2; // right
		if (p.y < bounds.min.y) code |= 4; // bottom
		else if (p.y > bounds.max.y) code |= 8; // top
		
		return code;
	},
	
	// square distance (to avoid unnecessary Math.sqrt calls)
	_sqDist: function(p1, p2) {
		var dx = p2.x - p1.x,
			dy = p2.y - p1.y;
		return dx * dx + dy * dy;
	},
	
	// square distance from point to a segment
	_sqPointToSegmentDist: function(p, p1, p2) {
		var x2 = p2.x - p1.x,
			y2 = p2.y - p1.y;
		
		if (!x2 && !y2) return this._sqDist(p, p1);
		
		var dot = (p.x - p1.x) * x2 + (p.y - p1.y) * y2,
			t = dot / this._sqDist(p1, p2);
		
		if (t < 0) return this._sqDist(p, p1);
		if (t > 1) return this._sqDist(p, p2);
		
		var proj = new L.Point(p1.x + x2 * t, p1.y + y2 * t);
		return this._sqDist(p, proj);
	}	
};