import { extend, isNil, pushIn } from '../core/util';
import { withInEllipse } from '../core/util/path';
import Coordinate from '../geo/Coordinate';
import CenterMixin from './CenterMixin';
import Polygon, { PolygonOptionsType, RingCoordinates, RingsCoordinates } from './Polygon';
import Circle from './Circle';
import Point from '../geo/Point';
import Extent from '../geo/Extent';
// https://zh.numberempire.com/graphingcalculator.php?functions=x%5E4&xmin=0&xmax=1&ymin=-1.0&ymax=1.0&var=x
function quarticIn(k: number) {
return k * k * k * k;
}
function angleT(numberOfShellPoints: number) {
//利用曲线方程,让角度的变化变成非线性
const fs: number[] = [];
// [0,90] 变化曲线
const ts1: number[] = [];
for (let i = 0; i < numberOfShellPoints; i++) {
ts1.push(quarticIn(i / numberOfShellPoints));
}
// [90,180] 变化曲线
const ts2 = ts1.map(t => {
return t;
}).reverse();
const ts: number[] = [];
pushIn(ts, ts1, ts2, ts1, ts2);
let sum = 0;
for (let i = 0, len = ts.length; i < len; i += 4) {
fs.push(ts[i]);
sum += ts[i];
}
return {
fs,
sum
};
}
/**
* @property {Object} [options=null]
* @property {Number} [options.numberOfShellPoints=60] - number of shell points when exporting the ellipse's shell coordinates as a polygon.
* @memberOf Ellipse
* @instance
*/
const options: EllipseOptionsType = {
'numberOfShellPoints': 81
};
/**
* 表示椭圆几何体
* @english
* Represents a Ellipse Geometry.
* @category geometry
* @extends Polygon
* @mixes CenterMixin
* @example
* var ellipse = new Ellipse([100, 0], 1000, 500, {
* id : 'ellipse0'
* });
*/
export class Ellipse extends CenterMixin(Polygon) {
width: number
height: number
options: EllipseOptionsType;
static fromJSON(json: Record): Ellipse {
const feature = json['feature'];
const ellipse = new Ellipse(json['coordinates'], json['width'], json['height'], json['options']);
ellipse.setProperties(feature['properties']);
return ellipse;
}
/**
* @param {Coordinate} center - center of the ellipse
* @param {Number} width - width of the ellipse, in meter
* @param {Number} height - height of the ellipse, in meter
* @param {Object} [options=null] - construct options defined in [Ellipse]{@link Ellipse#options}
*/
constructor(coordinates: Coordinate | Array, width: number, height: number, options?: EllipseOptionsType) {
super(null, options);
if (coordinates) {
this.setCoordinates(coordinates);
}
this.width = width;
this.height = height;
}
/**
* 获取椭圆的宽度
* @english
* Get ellipse's width
* @return {Number}
*/
getWidth(): number {
return this.width;
}
/**
* 设置椭圆的宽度
* Set new width to ellipse
* @param {Number} width - new width
* @fires Ellipse#shapechange
* @return {Ellipse} this
*/
setWidth(width: number) {
this.width = width;
this.onShapeChanged();
return this;
}
/**
* 获取椭圆高度
* @english
* Get ellipse's height
* @return {Number}
*/
getHeight(): number {
return this.height;
}
/**
* 设置椭圆高度
* @english
* Set new height to ellipse
* @param {Number} height - new height
* @fires Ellipse#shapechange
* @return {Ellipse} this
*/
setHeight(height: number) {
this.height = height;
this.onShapeChanged();
return this;
}
/**
* 获取作为多边形的椭圆的外壳,外壳点数由决定
* @english
* Gets the shell of the ellipse as a polygon, number of the shell points is decided by [options.numberOfShellPoints]{@link Circle#options}
* @return {Coordinate[]} - shell coordinates
*/
getShell(): RingCoordinates {
if (this.isRotated()) {
return this.getRotatedShell();
}
return this._getShell();
}
//@internal
_getShell(): RingCoordinates {
const measurer = this._getMeasurer(),
center = this.getCoordinates(),
numberOfPoints = this.options['numberOfShellPoints'] - 1,
width = this.getWidth(),
height = this.getHeight();
const shell = [];
const s = Math.pow(width / 2, 2) * Math.pow(height / 2, 2),
sx = Math.pow(width / 2, 2),
sy = Math.pow(height / 2, 2);
const angles = [];
if (Math.max(width / height, height / width) > 2) {
const { fs, sum } = angleT(numberOfPoints);
const dt = 360 / sum;
let offsetAngle = 0;
//Y > X
if (height > width) {
offsetAngle = 90;
}
let angle = 0;
for (let i = 0, len = fs.length; i < len; i++) {
angle += dt * fs[i];
angles.push(angle + offsetAngle);
}
} else {
for (let i = 0; i < numberOfPoints; i++) {
const angle = 360 * i / numberOfPoints;
angles.push(angle);
}
}
if (this.options.debug) {
console.log(angles);
}
let deg, rad, dx, dy;
for (let i = 0; i < angles.length; i++) {
deg = angles[i];
rad = deg * Math.PI / 180;
dx = Math.sqrt(s / (sx * Math.pow(Math.tan(rad), 2) + sy));
dy = Math.sqrt(s / (sy * Math.pow(1 / Math.tan(rad), 2) + sx));
if (deg > 90 && deg < 270) {
dx *= -1;
}
if (deg > 180 && deg < 360) {
dy *= -1;
}
const vertex = measurer.locate(center, dx, dy);
vertex.z = center.z;
shell.push(vertex);
}
shell.push(shell[0].copy());
return shell;
}
//@internal
_getPrjShell(): RingCoordinates {
const shell = super._getPrjShell();
return this._rotatePrjCoordinates(shell) as RingCoordinates;
}
/**
* 椭圆没有任何孔,总是返回null
* @english
* Ellipse won't have any holes, always returns null
* @return {Object[]} an empty array
*/
getHoles(): RingsCoordinates {
return [];
}
animateShow(): any {
return this.show();
}
//@internal
_containsPoint(point: Point, tolerance?: number): boolean {
const map = this.getMap();
if (map.isTransforming()) {
return super._containsPoint(point, tolerance);
}
const projection = map.getProjection();
const t = this._hitTestTolerance() + (tolerance || 0),
pps = projection.projectCoords([this._coordinates, map.locate(this._coordinates, this.getWidth() / 2, this.getHeight() / 2)], this.options['antiMeridian']),
p0 = map.prjToContainerPoint(pps[0] as Coordinate),
p1 = map.prjToContainerPoint(pps[1] as Coordinate);
return withInEllipse(point, p0, p1, t);
}
//@internal
_computePrjExtent(): Extent {
if (this.isRotated()) {
return this._computeRotatedPrjExtent();
}
// eslint-disable-next-line prefer-rest-params
return Circle.prototype._computePrjExtent.apply(this, arguments);
}
//@internal
_computeExtent(): any {
// eslint-disable-next-line prefer-rest-params
return Circle.prototype._computeExtent.apply(this, arguments);
}
//@internal
_getMinMax(measurer: any): [Coordinate, Coordinate, Coordinate, Coordinate] {
if (!measurer || !this._coordinates || isNil(this.width) || isNil(this.height)) {
return null;
}
const width = this.getWidth(),
height = this.getHeight();
const p1 = measurer.locate(this._coordinates, -width / 2, 0),
p2 = measurer.locate(this._coordinates, width / 2, 0),
p3 = measurer.locate(this._coordinates, 0, -height / 2),
p4 = measurer.locate(this._coordinates, 0, height / 2);
return [p1, p2, p3, p4];
}
//@internal
_computeGeodesicLength(): number {
if (isNil(this.width) || isNil(this.height)) {
return 0;
}
//L=2πb+4(a-b)
//近似值
const longer = (this.width > this.height ? this.width : this.height);
return 2 * Math.PI * longer / 2 - 4 * Math.abs(this.width - this.height);
}
//@internal
_computeGeodesicArea(): number {
if (isNil(this.width) || isNil(this.height)) {
return 0;
}
return Math.PI * this.width * this.height / 4;
}
//@internal
_exportGeoJSONGeometry() {
const coordinates = Coordinate.toNumberArrays([this.getShell()]);
return {
'type': 'Polygon',
'coordinates': coordinates
};
}
//@internal
_toJSON(options: any) {
const opts = extend({}, options);
const center = this.getCenter();
opts.geometry = false;
const feature = this.toGeoJSON(opts);
feature['geometry'] = {
'type': 'Polygon'
};
return {
'feature': feature,
'subType': 'Ellipse',
'coordinates': center.toArray(),
'width': this.getWidth(),
'height': this.getHeight()
};
}
}
Ellipse.mergeOptions(options);
Ellipse.registerJSONType('Ellipse');
export default Ellipse;
export type EllipseOptionsType = PolygonOptionsType & {
numberOfShellPoints?: number;
debug?: boolean;
}