import * as T from '@turf/turf'
import Geometry from './Geometry'
import { Log, defaultValue, defined, isNumber } from '../../util'
import Zondy from '../Zondy'
import { GeometryType, IGSGeometryType, RadiusUnit } from '../enum'
import Polygon from './Polygon'
import Point from './Point'
import SpatialReference from './SpatialReference'
import { calcExtent } from './Utiles'
import {
getProjUnitByWkid,
RadiusUnitToMeter,
ProjUnitWkidMap
} from '../../util/unit/UnitUtils'
/**
* 几何圆,参考示例:<a href='#Circle'>[几何圆对象]</a>
* <br><br>[ES5引入方式]:<br/>
* Zondy.Geometry.Circle() <br/>
* [ES6引入方式]:<br/>
* import { Circle } from "@mapgis/webclient-common" <br/>
* <br/>
* @class Circle
* @moduleEX GeometryModule
* @extends Geometry
* @param {Object} options 构造参数
* @param {Point|Number[]} [options.center] 圆心坐标
* @param {Number} [options.radius = 100] 圆心半径
* @param {RadiusUnit} [options.radiusUnit = 'meters'] 圆心半径单位
* @param {Number} [options.numberOfPoints = 40] 圆转换为区插值点个数
* @param {Number} [options.geodesic = false] 是否显示为地理圆减少失真。当圆spatialReference为4326或3857时,设置有true有效,其他坐标系默认显示为投影坐标系的标准圆。
* @param {SpatialReference} [options.spatialReference = new Zondy.SpatialReference('EPSG:4326')] 几何点的空间参考系,默认4326,当不是4326时请指定坐标系,方便进行投影转换,参考示例:<a href='#SpatialReference'>[指定坐标系]</a>
* @summary <h5>支持如下方法:</h5>
* <a href='#toPolygon'>[1、导出为区]</a><br/>
* <a href='#toString'>[2、返回字符串]</a><br/>
* <a href='#toXMl'>[3、导出为OGC服务要求的xml字符串]</a><br/>
* <a href='#getIGSType'>[4、返回IGS所对应的GeometryModule型]</a><br/>
* <a href='#fromJSON'>[5、通过传入的json构造并返回一个新的几何对象]</a><br/>
* <a href='#toJSON'>[6、导出为json对象]</a><br/>
* [7、克隆几何对象]{@link Geometry#clone}
*
* @example <caption><h7 id='Circle'>创建几何对象</h7></caption>
* // ES5引入方式
* const { Circle } = Zondy.Geometry
* // ES6引入方式
* import { Circle } from "@mapgis/webclient-common"
* new Circle({
* // 中心点
* center:[113,42],
* // 半径
* radius:20
* })
*
* @example <caption><h7 id='spatialReference'>指定坐标系</h7></caption>
* // ES5引入方式
* const { Circle } = Zondy.Geometry
* const { SpatialReference } = Zondy
* // ES6引入方式
* import { Circle, SpatialReference } from "@mapgis/webclient-common"
* new Circle({
* // 3857坐标系的点
* // 中心点
* center:[12060733.232006868, 3377247.5680546067],
* // 半径
* radius:10000,
* // 当不是4326时请指定坐标系,方便进行投影转换
* spatialReference: new SpatialReference('EPSG:3857')
* })
*/
class Circle extends Geometry {
constructor(options) {
super(options)
options = defaultValue(options, {})
/**
* 圆的中心点
* @member {Number[]} Circle.prototype.center
*/
this.center = defaultValue(options.center, undefined)
if (!this.center) {
throw new Error('缺少圆心坐标')
}
this.center = Point.toCoordinates(this.center)
/**
* 圆的半径
* @member {Number} Circle.prototype.radius
* @default 100
*/
this.radius = defaultValue(options.radius, 100)
if (!isNumber(this.radius)) {
throw new Error('半径必须为数字')
}
/**
* 是否显示为地理圆减少失真。当圆spatialReference为4326或3857时,设置有true有效,其他坐标系默认显示为投影坐标系的标准圆。
* @member {Boolean} Circle.prototype.geodesic
* @default false
*/
this.geodesic = defaultValue(options.geodesic, false)
/**
* 半径单位
* @member {RadiusUnit} Circle.prototype.radiusUnit
*/
this.radiusUnit = defaultValue(options.radiusUnit, RadiusUnit.degrees)
// 定义圆曲线上点的数量。
this.numberOfPoints = defaultValue(options.numberOfPoints, 40)
if (!isNumber(this.numberOfPoints)) {
throw new Error('圆曲线上点的数量必须为数字')
}
// 表示GeometryModule型的字符串值
this.type = GeometryType.circle
// 计算是否为三维
if (this.center.length === 3) {
this.hasZ = true
}
}
/**
* 通过传入的json构造并返回一个新的几何对象
* @param {Object} [json] JSON对象
* @example <caption><h7 id='fromJSON'>通过传入的json构造并返回一个新的几何对象</h7></caption>
* // ES5引入方式
* const { Circle } = Zondy.Geometry
* // ES6引入方式
* import { Circle } from "@mapgis/webclient-common"
* const json = {
* // 中心点
* center:[113, 42],
* // 半径
* radius:20
* }
* const circle = Circle.fromJSON(json)
*/
static fromJSON(json) {
json = defaultValue(json, {})
return new Circle(json)
}
/**
* <a id='toJSON'></a>
* 导出为json对象
* @return {Object} json对象
*/
toJSON() {
const json = super.toJSON()
json.center = JSON.parse(JSON.stringify(this.center))
json.radius = this.radius
json.radiusUnit = this.radiusUnit
json.numberOfPoints = this.numberOfPoints
json.extent = this.extent.toJSON()
return json
}
/**
* 导出为区
* @returns {Polygon} 返回区对象
* @example <caption><h7 id='toPolygon'>导出为区</h7></caption>
* // ES5引入方式
* const { Circle } = Zondy.Geometry
* // ES6引入方式
* import { Circle } from "@mapgis/webclient-common"
* const circle = new Circle({
* // 中心点
* center:[113, 42],
* // 半径
* radius:20
* })
* const polygon = circle.toPolygon()
*/
toPolygon() {
// 先确定当前参考系时地理坐标系还是投影坐标系
const spatialReference = this.spatialReference
const geodesic = this.geodesic
const unitRatio = RadiusUnitToMeter[this.radiusUnit] || 1
// 将半径转换为m
let radius = this.radius * unitRatio
let currentSpDesc = 'geographic'
if (spatialReference.isWebMercator) {
currentSpDesc = 'webMercator'
} else if (
spatialReference.wkid &&
defined(ProjUnitWkidMap[spatialReference.wkid])
) {
currentSpDesc = 'proj'
} else if (spatialReference.wkt) {
const wkt = spatialReference.wkt
// 兼容各种版本的wkt和projJS
const map = ['PROJS', 'proj4', 'PROJCRS', '+proj']
const isProj = map.some((v) => wkt.indexOf(v) === 0)
if (isProj) {
currentSpDesc = 'proj'
}
}
if (geodesic) {
switch (currentSpDesc) {
case 'proj': {
Log.error('暂时仅支持墨卡托投影坐标系和地理坐标系使用geodesic属性')
break
}
case 'webMercator':
case 'geographic': {
// 计算真实的地理圆
// link https://stackoverflow.com/questions/37599561/drawing-a-circle-with-the-radius-in-miles-meters-with-mapbox-gl-js/39006388#39006388
const max = 85.0511287798
const R = 6378137
const d = Math.PI / 180
let coords = {
latitude: this.center[1],
longitude: this.center[0]
}
if (currentSpDesc === 'webMercator') {
coords = {
longitude: (coords.longitude * 180) / Math.PI / R,
latitude:
((2 * Math.atan(Math.exp(coords.latitude / R)) - Math.PI / 2) *
180) /
Math.PI
}
}
const km = radius / 1000
const distanceX =
km / (111.32 * Math.cos((coords.latitude * Math.PI) / 180))
const distanceY = km / 110.574
const lnglatArr = []
for (let i = 0; i < this.numberOfPoints; i += 1) {
const theta = (i / this.numberOfPoints) * (2 * Math.PI)
const x = distanceX * Math.cos(theta)
const y = distanceY * Math.sin(theta)
const lng = coords.longitude + x
const lat = coords.latitude + y
if (currentSpDesc === 'webMercator') {
const _lat = Math.max(Math.min(85.0511287798, lat), -max)
const sin = Math.sin(_lat * d)
lnglatArr.push([
R * lng * d,
(R * Math.log((1 + sin) / (1 - sin))) / 2
])
} else {
lnglatArr.push([lng, lat])
}
}
if (lnglatArr.length <= 3) Log.error('生成圆几何点个数不能小于3')
// 首位点闭合
lnglatArr.push(lnglatArr[0])
return new Polygon({
coordinates: [lnglatArr],
spatialReference: SpatialReference.fromJSON(this.spatialReference)
})
}
default: {
break
}
}
} else {
if (currentSpDesc === 'geographic') {
radius /= RadiusUnitToMeter['degrees']
}
const coordinates = this._tranCircleToPath(
this.center,
radius,
this.numberOfPoints
)
return new Polygon({
coordinates,
spatialReference: SpatialReference.fromJSON(this.spatialReference)
})
}
}
/**
* 返回如下格式的字符串:"x,y,radius",若有z,则返回"x,y,z,radius"<a id='toString'></a>
* @returns {String} 返回几何字符串
*/
toString() {
if (this.hasZ) {
return `${this.center[0]},${this.center[1]},${this.center[2]},${this.radius}`
}
return `${this.center[0]},${this.center[1]},${this.radius}`
}
/**
* 导出为OGC服务要求的xml字符串<a id='toXMl'></a>
* @return {String} 字符串
*/
toXMl() {
return ''
}
/**
* 返回IGS所对应的GeometryModule型<a id='getIGSType'></a>
* @returns {String} GeometryModule型
*/
getIGSType() {
return IGSGeometryType.circle
}
/**
* @function Circle.prototype._tranCircleToPath
* @private
* @description 转换圆为区
* @returns {Number[][]} 返回转换后的坐标数组
*/
_tranCircleToPath(center, radius, numberOfPoints) {
const disRad = (Math.PI * 2) / numberOfPoints
let angle = 0
let coordinates = []
const coords = []
for (let i = 0; i < numberOfPoints; i++) {
angle -= disRad // clockwise
const point = [
center[0] + radius * Math.cos(angle),
center[1] + radius * Math.sin(angle)
]
coords.push(point)
}
const last = coords[coords.length - 1]
const first = coords[0]
if (
Math.abs(last[0] - first[0]) > 10e-8 ||
Math.abs(last[1] - first[1]) > 10e-8
) {
coords.push([first[0], first[1]])
}
coordinates.push(coords)
if (this.hasZ) {
coordinates = coordinates.map((ring) => {
return ring.map((coordinate) => {
return [coordinate[0], coordinate[1], center[2]]
})
})
}
return coordinates
}
/**
* @function Circle.prototype._calcExtent
* @private
* @description 计算外包盒
* @param {Boolean} hasZ 是否是三维
* @returns {Extent} 外包盒
*/
_calcExtent(hasZ) {
const polygon = this.toPolygon()
return calcExtent(polygon._cloneCoordinates(), hasZ)
}
/**
* 克隆几何对象
* @return {Geometry} 克隆后的几何对象
*/
clone() {
return new Circle(this.toJSON())
}
}
Object.defineProperties(Circle.prototype, {
/**
* 外包盒
* @member {Number} Circle.prototype.extent
* */
extent: {
configurable: false,
get() {
return this._calcExtent(this.hasZ)
}
}
})
Zondy.Geometry.Circle = Circle
export default Circle