Leaflet源码解析系列(三):Geometry 类型解读

196 阅读7分钟

Geometry中的类没有继承自Class,定义了基础的数据类型对象。

点 Point

表示一个点,x、y以像素为坐标。可以进行加、减、乘、除、缩放、取整等计算,也可以进行距离计算、相等判断、大小比较。

import {formatNum} from '../core/Util';

/*
 * @class Point
 * @aka L.Point
 *
 * 用 `x` and `y` 表示一个 像素坐标系 中的点 
 *
 * @example
 *
 * ```js
 * var point = L.point(200, 300);
 * ```
 *
 * Leaflet中所有接受 Point 对象的方法和 option 都可以接受一个简单的数组 (除非有特别说明), 下面的代码是等效的:
 *
 * ```js
 * map.panBy([200, 300]);
 * map.panBy(L.point(200, 300));
 * ```
 *
 * 注意: Point 没有继承自 Class 对象,因此 point 也不能扩展子类,也不能用 includes 扩展 point
 */

export function Point(x, y, round) {
	// @property x: Number; The `x` coordinate of the point
	this.x = (round ? Math.round(x) : x);
	// @property y: Number; The `y` coordinate of the point
	this.y = (round ? Math.round(y) : y);
}

const trunc = Math.trunc || function (v) {
	return v > 0 ? Math.floor(v) : Math.ceil(v);
};

Point.prototype = {

	// @method clone(): Point
	// Returns a copy of the current point.
	clone() {
		return new Point(this.x, this.y);
	},

	// @method add(otherPoint: Point): Point
	// Returns the result of addition of the current and the given points.
	add(point) {
		// 不会改变当前point, 返回了新的 point
		return this.clone()._add(toPoint(point));
	},

	_add(point) {
		// 具有破坏性,会改变当前point, used directly for performance in situations where it's safe to modify existing point
		this.x += point.x;
		this.y += point.y;
		return this;
	},

	// @method subtract(otherPoint: Point): Point
	// 当前减去point,返回结果.
	subtract(point) {
		return this.clone()._subtract(toPoint(point));
	},

	_subtract(point) {
		this.x -= point.x;
		this.y -= point.y;
		return this;
	},

	// @method divideBy(num: Number): Point
	// Returns the result of division of the current point by the given number.
	divideBy(num) {
		return this.clone()._divideBy(num);
	},

	_divideBy(num) {
		this.x /= num;
		this.y /= num;
		return this;
	},

	// @method multiplyBy(num: Number): Point
	// Returns the result of multiplication of the current point by the given number.
	multiplyBy(num) {
		return this.clone()._multiplyBy(num);
	},

	_multiplyBy(num) {
		this.x *= num;
		this.y *= num;
		return this;
	},

	// @method scaleBy(scale: Point): Point
	// Multiply each coordinate of the current point by each coordinate of
	// `scale`. In linear algebra terms, multiply the point by the
	// [scaling matrix](https://en.wikipedia.org/wiki/Scaling_%28geometry%29#Matrix_representation)
	// defined by `scale`.
	scaleBy(point) {
		return new Point(this.x * point.x, this.y * point.y);
	},

	// @method unscaleBy(scale: Point): Point
	// Inverse of `scaleBy`. Divide each coordinate of the current point by
	// each coordinate of `scale`.
	unscaleBy(point) {
		return new Point(this.x / point.x, this.y / point.y);
	},

	// @method round(): Point
	// Returns a copy of the current point with rounded coordinates.
	round() {
		return this.clone()._round();
	},

	_round() {
		this.x = Math.round(this.x);
		this.y = Math.round(this.y);
		return this;
	},

	// @method floor(): Point
	// Returns a copy of the current point with floored coordinates (rounded down).
	floor() {
		return this.clone()._floor();
	},

	_floor() {
		this.x = Math.floor(this.x);
		this.y = Math.floor(this.y);
		return this;
	},

	// @method ceil(): Point
	// Returns a copy of the current point with ceiled coordinates (rounded up).
	ceil() {
		return this.clone()._ceil();
	},

	_ceil() {
		this.x = Math.ceil(this.x);
		this.y = Math.ceil(this.y);
		return this;
	},

	// @method trunc(): Point
	// Returns a copy of the current point with truncated coordinates (rounded towards zero).
  // 删除小数位数
	trunc() {
		return this.clone()._trunc();
	},

	_trunc() {
		this.x = trunc(this.x);
		this.y = trunc(this.y);
		return this;
	},

	// @method distanceTo(otherPoint: Point): Number
	// Returns the cartesian distance between the current and the given points.
	distanceTo(point) {
		point = toPoint(point);

		const x = point.x - this.x,
		    y = point.y - this.y;

		return Math.sqrt(x * x + y * y);
	},

	// @method equals(otherPoint: Point): Boolean
	// Returns `true` if the given point has the same coordinates.
	equals(point) {
		point = toPoint(point);

		return point.x === this.x &&
		       point.y === this.y;
	},

	// @method contains(otherPoint: Point): Boolean
	// Returns `true` if both coordinates of the given point are less than the corresponding current point coordinates (in absolute values).
	contains(point) {
		point = toPoint(point);

		return Math.abs(point.x) <= Math.abs(this.x) &&
		       Math.abs(point.y) <= Math.abs(this.y);
	},

	// @method toString(): String
	// Returns a string representation of the point for debugging purposes.
	toString() {
    // formatNum对小数最后一位做了四舍五入
		return `Point(${formatNum(this.x)}, ${formatNum(this.y)})`;
	}
};

// @factory L.point(x: Number, y: Number, round?: Boolean)
// Creates a Point object with the given `x` and `y` coordinates. If optional `round` is set to true, rounds the `x` and `y` values.

// @alternative
// @factory L.point(coords: Number[])
// Expects an array of the form `[x, y]` instead.

// @alternative
// @factory L.point(coords: Object)
// Expects a plain object of the form `{x: Number, y: Number}` instead.
// 根据接受的参数格式,按照 Point、Array、Object先后判断类型
export function toPoint(x, y, round) {
	if (x instanceof Point) {
		return x;
	}
	if (Array.isArray(x)) {
		return new Point(x[0], x[1]);
	}
	if (x === undefined || x === null) {
		return x;
	}
	if (typeof x === 'object' && 'x' in x && 'y' in x) {
		return new Point(x.x, x.y);
	}
	return new Point(x, y, round);
}

边界 bounds

表示像素坐标中的矩形区域。可以确定一个四至范围。

import {Point, toPoint} from './Point';

/*
 * @class Bounds
 * @aka L.Bounds
 *
 * 表示像素坐标系中的一个矩形范围.
 *
 * @example
 *
 * ```js
 * var p1 = L.point(10, 10),
 * p2 = L.point(40, 60),
 * bounds = L.bounds(p1, p2);
 * ```
 *
 * Leaflet 接受 Bounds 参数的方法都接受一个简单的数组作为参数(除非有特殊说明),下面的代码是等效的
 *
 * ```js
 * otherBounds.intersects([[10, 10], [40, 60]]);
 * ```
 *
 * 注意: Bounds 没有继承自 Class 对象,因此 point 也不能扩展子类,也不能用 includes 扩展 point
 */

export function Bounds(a, b) {
	if (!a) { return; }

	const points = b ? [a, b] : a;

	for (let i = 0, len = points.length; i < len; i++) {
		this.extend(points[i]);
	}
}

Bounds.prototype = {
	// @method extend(point: Point): this
	// Extends the bounds to contain the given point.

	// @alternative
	// @method extend(otherBounds: Bounds): this
	// Extend the bounds to contain the given bounds
	extend(obj) {
		let min2, max2;
		if (!obj) { return this; }

		if (obj instanceof Point || typeof obj[0] === 'number' || 'x' in obj) {
      // 传入的不是Bounds,是(point 数组 或者 对象)
			min2 = max2 = toPoint(obj);
		} else {
			obj = toBounds(obj);
			min2 = obj.min;
			max2 = obj.max;

      // 为啥要用 ! || !,而不用 && ?
			if (!min2 || !max2) { return this; }
		}

		// @property min: Point
		// The top left corner of the rectangle.
		// @property max: Point
		// The bottom right corner of the rectangle.
		if (!this.min && !this.max) {
			this.min = min2.clone();
			this.max = max2.clone();
		} else {
			this.min.x = Math.min(min2.x, this.min.x);
			this.max.x = Math.max(max2.x, this.max.x);
			this.min.y = Math.min(min2.y, this.min.y);
			this.max.y = Math.max(max2.y, this.max.y);
		}
		return this;
	},

	// @method getCenter(round?: Boolean): Point
	// 获取中心点
	getCenter(round) {
		return toPoint(
		        (this.min.x + this.max.x) / 2,
		        (this.min.y + this.max.y) / 2, round);
	},

	// @method getBottomLeft(): Point
	// 获取左下角
	getBottomLeft() {
		return toPoint(this.min.x, this.max.y);
	},

	// @method getTopRight(): Point
	// 获取右上角
	getTopRight() { // -> Point
		return toPoint(this.max.x, this.min.y);
	},

	// @method getTopLeft(): Point
	// 获取做上角,因为是像素坐标,所以mix就是左上角
	getTopLeft() {
		return this.min; // left, top
	},

	// @method getBottomRight(): Point
	// 获取右下角
	getBottomRight() {
		return this.max; // right, bottom
	},

	// @method getSize(): Point
	// 返回 x y 方向的长度,像素单位
	getSize() {
		return this.max.subtract(this.min);
	},

	// @method contains(otherBounds: Bounds): Boolean
	// 判断 Bounds 是否包含另一个 Bounds
	// @alternative
	// @method contains(point: Point): Boolean
	// 判断 Bounds 是否包含以个点
	contains(obj) {
		let min, max;

    // 区分传入参数的类型
		if (typeof obj[0] === 'number' || obj instanceof Point) {
			obj = toPoint(obj);
		} else {
			obj = toBounds(obj);
		}

		if (obj instanceof Bounds) {
			min = obj.min;
			max = obj.max;
		} else {
			min = max = obj;
		}

		return (min.x >= this.min.x) &&
		       (max.x <= this.max.x) &&
		       (min.y >= this.min.y) &&
		       (max.y <= this.max.y);
	},

	// @method intersects(otherBounds: Bounds): Boolean
	// 判断两个 Bounds 是否相交,只要有一个像素点重叠了,就认为两个算相交。
	intersects(bounds) { // (Bounds) -> Boolean
		bounds = toBounds(bounds);

		const min = this.min,
		    max = this.max,
		    min2 = bounds.min,
		    max2 = bounds.max,
		    xIntersects = (max2.x >= min.x) && (min2.x <= max.x),
		    yIntersects = (max2.y >= min.y) && (min2.y <= max.y);

		return xIntersects && yIntersects;
	},

	// @method overlaps(otherBounds: Bounds): Boolean
	// 判断两个 Bounds 是否相交。两个 Bounds的重叠区域要能构成面才算重叠。
	overlaps(bounds) { // (Bounds) -> Boolean
		bounds = toBounds(bounds);

		const min = this.min,
		    max = this.max,
		    min2 = bounds.min,
		    max2 = bounds.max,
		    xOverlaps = (max2.x > min.x) && (min2.x < max.x),
		    yOverlaps = (max2.y > min.y) && (min2.y < max.y);

		return xOverlaps && yOverlaps;
	},

	// @method isValid(): Boolean
	// 判断是否存在逻辑错误
	isValid() {
		return !!(this.min && this.max);
	},


	// @method pad(bufferRatio: Number): Bounds
  // 扩大或者缩小一定比例的 Bounds,保持中心不变。参数为负值的时候是缩小。
	pad(bufferRatio) {
		const min = this.min,
		max = this.max,
		heightBuffer = Math.abs(min.x - max.x) * bufferRatio,
		widthBuffer = Math.abs(min.y - max.y) * bufferRatio;


		return toBounds(
			toPoint(min.x - heightBuffer, min.y - widthBuffer),
			toPoint(max.x + heightBuffer, max.y + widthBuffer));
	},


	// @method equals(otherBounds: Bounds): Boolean
	// 判断两个 Bounds 的范围是否一致
	equals(bounds) {
		if (!bounds) { return false; }

		bounds = toBounds(bounds);

		return this.min.equals(bounds.getTopLeft()) &&
			this.max.equals(bounds.getBottomRight());
	},
};


// @factory L.bounds(corner1: Point, corner2: Point)
// Creates a Bounds object from two corners coordinate pairs.
// @alternative
// @factory L.bounds(points: Point[])
// Creates a Bounds object from the given array of points.
export function toBounds(a, b) {
	if (!a || a instanceof Bounds) {
		return a;
	}
	return new Bounds(a, b);
}

换算 Transformation

仿射变换:一组系数a,b,c,d,用于将形式(x,y)的点变换为(ax+b,cy+d)并进行相反的变换。由Leaflet 在坐标投影变换的时候使用。(参见 Geo 中的 CRS 的 transformation )

import {Point} from './Point';

/*
 * @class Transformation
 * @aka L.Transformation
 *
 * 表示仿射变换:一组系数a,b,c,d
 * 用于将形式(x,y)的点变换为(x*a+b,y*c+d)并进行相反的变换。
 * Leaflet 在投影变换相关的代码中使用
 *
 * @example
 *
 * ```js
 * var transformation = L.transformation(2, 5, -1, 10),
 * 	p = L.point(1, 2),
 * 	p2 = transformation.transform(p), //  L.point(7, 8)
 * 	p3 = transformation.untransform(p2); //  L.point(1, 2)
 * ```
 */


// factory new L.Transformation(a: Number, b: Number, c: Number, d: Number)
// 使用给定的系数创建一个 Transformation
export function Transformation(a, b, c, d) {
	if (Array.isArray(a)) {
		// use array properties
		this._a = a[0];
		this._b = a[1];
		this._c = a[2];
		this._d = a[3];
		return;
	}
	this._a = a;
	this._b = b;
	this._c = c;
	this._d = d;
}

Transformation.prototype = {
	// @method transform(point: Point, scale?: Number): Point
	// Returns a transformed point, optionally multiplied by the given scale.
	// 只接受 Point,不接受数组.
	transform(point, scale) { // (Point, Number) -> Point
		return this._transform(point.clone(), scale);
	},

	// destructive transform (faster)
	_transform(point, scale) {
		scale = scale || 1;
		point.x = scale * (this._a * point.x + this._b);
		point.y = scale * (this._c * point.y + this._d);
		return point;
	},

	// @method untransform(point: Point, scale?: Number): Point
	// Returns the reverse transformation of the given point, optionally divided
	// by the given scale. Only accepts actual `L.Point` instances, not arrays.
	untransform(point, scale) {
		scale = scale || 1;
		return new Point(
		        (point.x / scale - this._b) / this._a,
		        (point.y / scale - this._d) / this._c);
	}
};

// factory L.transformation(a: Number, b: Number, c: Number, d: Number)

// @factory L.transformation(a: Number, b: Number, c: Number, d: Number)
// Instantiates a Transformation object with the given coefficients.

// @alternative
// @factory L.transformation(coefficients: Array): Transformation
// Expects an coefficients array of the form
// `[a: Number, b: Number, c: Number, d: Number]`.

export function toTransformation(a, b, c, d) {
	return new Transformation(a, b, c, d);
}

线LineUtil.js

用来简化和裁剪处理线要素相关的方法,用于加快leaflet的渲染性能。

import {Point, toPoint} from './Point';
import {toLatLng} from '../geo/LatLng';


/*
 * @namespace LineUtil
 *
 * 处理 polyline 中 points 的一系列工具函数, used by Leaflet internally to make polylines lightning-fast.
 */

// 使用 vertex reduction and Douglas-Peucker simplification简化线.
// Improves rendering performance dramatically by lessening the number of points to draw.

// @function simplify(points: Point[], tolerance: Number): Point[]
// 在保留线形状的同时显著地减少线段的点,返回一个新的线段, 
// 使用道格拉斯普克算法[Ramer-Douglas-Peucker algorithm](https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm).
// 用来大幅提升各个缩放级别下线段的渲染和显示性能,同时也能解决“视觉噪点”
// tolerance 通过设定一个距离用来控制简化效果(值越小意味着质量越高,但速度越慢,点数越多).
// 这个工具也发布了一个独立的工具库 [Simplify.js](https://mourner.github.io/simplify-js/).
export function simplify(points, tolerance) {
	if (!tolerance || !points.length) {
		return points.slice();
	}

  // 预先计算平方,在下面做比较的时候就不用求平方根了,增加计算效率
	const sqTolerance = tolerance * tolerance;

	    // 第一步 1: 按距离减少点
	    points = _reducePoints(points, sqTolerance);

	    // 第二步 2: Douglas-Peucker 道格拉斯普克算法简化
	    points = _simplifyDP(points, sqTolerance);

	return points;
}

// @function pointToSegmentDistance(p: Point, p1: Point, p2: Point): Number
//求点到线段的最近距离
export function pointToSegmentDistance(p, p1, p2) {
	return Math.sqrt(_sqClosestPointOnSegment(p, p1, p2, true));
}

// @function closestPointOnSegment(p: Point, p1: Point, p2: Point): Number
// 求点到线段的最近点
export function closestPointOnSegment(p, p1, p2) {
	return _sqClosestPointOnSegment(p, p1, p2);
}

// Ramer-Douglas-Peucker 简化算法, see https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm
function _simplifyDP(points, sqTolerance) {

	const len = points.length,
	    ArrayConstructor = typeof Uint8Array !== `${undefined}` ? Uint8Array : Array,
	    markers = new ArrayConstructor(len);

	    markers[0] = markers[len - 1] = 1;

	_simplifyDPStep(points, markers, sqTolerance, 0, len - 1);

	let i;
	const newPoints = [];

	for (i = 0; i < len; i++) {
		if (markers[i]) {
			newPoints.push(points[i]);
		}
	}

	return newPoints;
}

function _simplifyDPStep(points, markers, sqTolerance, first, last) {

	let maxSqDist = 0,
	index, i, sqDist;

	for (i = first + 1; i <= last - 1; i++) {
		sqDist = _sqClosestPointOnSegment(points[i], points[first], points[last], true);

		if (sqDist > maxSqDist) {
			index = i;
			maxSqDist = sqDist;
		}
	}

	if (maxSqDist > sqTolerance) {
		markers[index] = 1;

		_simplifyDPStep(points, markers, sqTolerance, first, index);
		_simplifyDPStep(points, markers, sqTolerance, index, last);
	}
}

// 将彼此距离太近的点简化成一个
function _reducePoints(points, sqTolerance) {
  // 保存结果数据,第一个点不会被简化掉
	const reducedPoints = [points[0]];
	let prev = 0;

	for (let i = 1; i < points.length; i++) {
    // 遍历所有的点,
		if (_sqDist(points[i], points[prev]) > sqTolerance) {
      // 如果两个点之间的距离大于限差,就保存到结果数组中(小于限差的都被剔除了)
			reducedPoints.push(points[i]);
      // 把当前点置为上一个点的索引
			prev = i;
		}
	}
	if (prev < points.length - 1) {
    // 最后一个如果没有被选入,prev就会小于points.length
    // 但是最后一个点不被简化,所有手动加进来
		reducedPoints.push(points[points.length - 1]);
	}
	return reducedPoints;
}

let _lastCode;

// @function clipSegment(a: Point, b: Point, bounds: Bounds, useLastCode?: Boolean, round?: Boolean): Point[]|Boolean
// 使用Cohen-Sutherland算法对线段进行按照矩形裁剪
// [Cohen-Sutherland algorithm](https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm)
// 直接修改线段的点,用于leaflet在图上快速渲染线要素
export function clipSegment(a, b, bounds, useLastCode, round) {
	let codeA = useLastCode ? _lastCode : _getBitCode(a, bounds),
	    codeB = _getBitCode(b, bounds),

	    codeOut, p, newCode;

	    // save 2nd code to avoid calculating it on the next segment
	    _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)
		if (codeA & codeB) {
			return false;
		}

		// other cases
		codeOut = codeA || codeB;
		p = _getEdgeIntersection(a, b, codeOut, bounds, round);
		newCode = _getBitCode(p, bounds);

		if (codeOut === codeA) {
			a = p;
			codeA = newCode;
		} else {
			b = p;
			codeB = newCode;
		}
	}
}

// 获取相交
export function _getEdgeIntersection(a, b, code, bounds, round) {
	const dx = b.x - a.x,
	      dy = b.y - a.y,
	      min = bounds.min,
	      max = bounds.max;
	let x, y;

	if (code & 8) { // top
		x = a.x + dx * (max.y - a.y) / dy;
		y = max.y;

	} else if (code & 4) { // bottom
		x = a.x + dx * (min.y - a.y) / dy;
		y = min.y;

	} else if (code & 2) { // right
		x = max.x;
		y = a.y + dy * (max.x - a.x) / dx;

	} else if (code & 1) { // left
		x = min.x;
		y = a.y + dy * (min.x - a.x) / dx;
	}

	return new Point(x, y, round);
}

export function _getBitCode(p, bounds) {
	let code = 0;

	if (p.x < bounds.min.x) { // left
		code |= 1;
	} else if (p.x > bounds.max.x) { // right
		code |= 2;
	}

	if (p.y < bounds.min.y) { // bottom
		code |= 4;
	} else if (p.y > bounds.max.y) { // top
		code |= 8;
	}

	return code;
}

// 平方距离 (用平方距离作比较可以避免很多不必要的 Math.sqrt 运算)
function _sqDist(p1, p2) {
	const dx = p2.x - p1.x,
	    dy = p2.y - p1.y;
	return dx * dx + dy * dy;
}

// 求 p 点到线段 p1-p2 的最近距离或者最近点??
export function _sqClosestPointOnSegment(p, p1, p2, sqDist) {
  // https://blog.csdn.net/qq_28087491/article/details/119239974
	let x = p1.x,
	    y = p1.y,
	    dx = p2.x - x,
	    dy = p2.y - y,
	    t;
  // p1-p2 的距离平方
	const dot = dx * dx + dy * dy;

	// 如果dot等于0,表示p1等于p2,表示线段是一个点
  if (dot > 0) {
    // p到p1的距离 占 p1-p2距离的比值
		t = ((p.x - x) * dx + (p.y - y) * dy) / dot;

		if (t > 1) {
      // p在p1-p2上或p1-p2延长线上
			x = p2.x;
			y = p2.y;
		} else if (t > 0) {
      // p在p1-p2之间,未落在线上
			x += dx * t;
			y += dy * t;
		}
	}

  // 垂线交点
	dx = p.x - x;
	dy = p.y - y;

	return sqDist ? dx * dx + dy * dy : new Point(x, y);
}


// @function isFlat(latlngs: LatLng[]): Boolean
// Returns true if `latlngs` is a flat array, false is nested.
export function isFlat(latlngs) {
	return !Array.isArray(latlngs[0]) || (typeof latlngs[0][0] !== 'object' && typeof latlngs[0][0] !== 'undefined');
}

/* @function polylineCenter(latlngs: LatLng[], crs: CRS): LatLng
 * Returns the center ([centroid](http://en.wikipedia.org/wiki/Centroid)) of the passed LatLngs (first ring) from a polyline.
 */
export function polylineCenter(latlngs, crs) {
	let i, halfDist, segDist, dist, p1, p2, ratio, center;

	if (!latlngs || latlngs.length === 0) {
		throw new Error('latlngs not passed');
	}

	if (!isFlat(latlngs)) {
		console.warn('latlngs are not flat! Only the first ring will be used');
		latlngs = latlngs[0];
	}

	const points = [];
	for (const j in latlngs) {
		points.push(crs.project(toLatLng(latlngs[j])));
	}

	const len = points.length;

	for (i = 0, halfDist = 0; i < len - 1; i++) {
		halfDist += points[i].distanceTo(points[i + 1]) / 2;
	}

	// The line is so small in the current view that all points are on the same pixel.
	if (halfDist === 0) {
		center = points[0];
	} else {
		for (i = 0, dist = 0; i < len - 1; i++) {
			p1 = points[i];
			p2 = points[i + 1];
			segDist = p1.distanceTo(p2);
			dist += segDist;

			if (dist > halfDist) {
				ratio = (dist - halfDist) / segDist;
				center = [
					p2.x - ratio * (p2.x - p1.x),
					p2.y - ratio * (p2.y - p1.y)
				];
				break;
			}
		}
	}
	return crs.unproject(toPoint(center));
}

面 PolyUtil

import * as LineUtil from './LineUtil';
import {toLatLng} from '../geo/LatLng';
import {toPoint} from './Point';
/*
 * @namespace PolyUtil
 * polygon geometries 的工具函数.
 */

/* @function clipPolygon(points: Point[], bounds: Bounds, round?: Boolean): Point[]
 * 使用指定的bounds裁剪面 (使用的是[Sutherland-Hodgman 算法](https://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm)).
 * 用于绘制屏幕范围内的要素,加快性能。
 * 面和线需要不同的裁剪算法,所以这里线和面的方法是分开的。
 */
export function clipPolygon(points, bounds, round) {
	let clippedPoints,
	    i, j, k,
	    a, b,
	    len, edge, p;
	const edges = [1, 4, 2, 8];

	for (i = 0, len = points.length; i < len; i++) {
		points[i]._code = LineUtil._getBitCode(points[i], bounds);
	}

	// for each edge (left, bottom, right, top)
	for (k = 0; k < 4; k++) {
		edge = edges[k];
		clippedPoints = [];

		for (i = 0, len = points.length, j = len - 1; i < len; j = i++) {
			a = points[i];
			b = points[j];

			// if a is inside the clip window
			if (!(a._code & edge)) {
				// if b is outside the clip window (a->b goes out of screen)
				if (b._code & edge) {
					p = LineUtil._getEdgeIntersection(b, a, edge, bounds, round);
					p._code = LineUtil._getBitCode(p, bounds);
					clippedPoints.push(p);
				}
				clippedPoints.push(a);

			// else if b is inside the clip window (a->b enters the screen)
			} else if (!(b._code & edge)) {
				p = LineUtil._getEdgeIntersection(b, a, edge, bounds, round);
				p._code = LineUtil._getBitCode(p, bounds);
				clippedPoints.push(p);
			}
		}
		points = clippedPoints;
	}

	return points;
}

/* @function polygonCenter(latlngs: LatLng[] crs: CRS): LatLng
 * 返回面的质心 ([centroid](http://en.wikipedia.org/wiki/Centroid))
 */
export function polygonCenter(latlngs, crs) {
	let i, j, p1, p2, f, area, x, y, center;

	if (!latlngs || latlngs.length === 0) {
		throw new Error('latlngs not passed');
	}

	if (!LineUtil.isFlat(latlngs)) {
		console.warn('latlngs are not flat! Only the first ring will be used');
		latlngs = latlngs[0];
	}

	const points = [];
	for (const k in latlngs) {
		points.push(crs.project(toLatLng(latlngs[k])));
	}

	const len = points.length;
	area = x = y = 0;

	// polygon centroid algorithm;
	for (i = 0, j = len - 1; i < len; j = i++) {
		p1 = points[i];
		p2 = points[j];

		f = p1.y * p2.x - p2.y * p1.x;
		x += (p1.x + p2.x) * f;
		y += (p1.y + p2.y) * f;
		area += f * 3;
	}

	if (area === 0) {
		// Polygon is so small that all points are on same pixel.
		center = points[0];
	} else {
		center = [x / area, y / area];
	}
	return crs.unproject(toPoint(center));
}