类名 common/base/geometry/Circle.js
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
构造函数
成员变量
方法
事件