类名 common/base/geometry/Polygon.js
import * as T from '@turf/turf'
import Geometry from './Geometry'
import { defaultValue, isNumber } from '../../util'
import Zondy from '../Zondy'
import { GeometryType, IGSGeometryType } from '../enum'
import Point from './Point'
import Extent from './Extent'
import { calcExtent } from './Utiles'

/**
 * 区几何,由多个环(ring)构成的几何对象,仅支持带洞区和非带洞区,不支持带岛区,即三个圈重叠<br/>
 * 多边形的第一个环(ring)即外圈,从第二个环开始为内圈,内圈可以重合、相交、自相交,但是不能超过外圈
 * <br><br>[ES5引入方式]:<br/>
 * Zondy.Geometry.Polygon() <br/>
 * [ES6引入方式]:<br/>
 * import { Polygon } from "@mapgis/webclient-common" <br/>
 * <br/>
 * @class Polygon
 * @moduleEX GeometryModule
 * @extends Geometry
 * @param {Object} options 构造参数
 * @param {Array} [options.coordinates = []] 几何点的坐标数组,参考示例如下:<br/>
 * <a href='#Polygon'>[1、不带洞的区几何对象]</a><br/>
 * <a href='#Polygon-innder'>[2、带洞的区几何对象]</a>
 * @param {SpatialReference} [options.spatialReference = new Zondy.SpatialReference('EPSG:4326')] 几何点的空间参考系,默认4326,当不是4326时请指定坐标系,方便进行投影转换,参考示例:<a href='#SpatialReference'>[指定坐标系]</a>
 * @summary <h5>支持如下方法:</h5>
 * <a href='#fromExtent'>[1、将给定的Extent转换为多边形实例]</a><br/>
 * <a href='#addRing'>[2、在多边形中添加一个环]</a><br/>
 * <a href='#removeRing'>[3、根据索引从多边形中移除一个环]</a><br/>
 * <a href='#contains'>[4、检查输入点是否在多边形内]</a><br/>
 * <a href='#getPoint'>[5、根据下标返回指定的点]</a><br/>
 * <a href='#insertPoint'>[6、在多边形中插入一个新点]</a><br/>
 * <a href='#removePoint'>[7、根据下标从多边形中移除一个点]</a><br/>
 * <a href='#toXMl'>[8、导出为OGC服务要求的xml字符串]</a><br/>
 * <a href='#toString'>[9、返回字符串]</a><br/>
 * <a href='#toOldIGSGeometry'>[10、返回igs1.0的几何对象]</a><br/>
 * <a href='#getIGSType'>[11、返回IGS所对应的GeometryModule型]</a><br/>
 * <a href='#toDots'>[12、返回Dots对象,仅包括多边形的外圈]</a>
 * <a href='#fromJSON'>[13、通过传入的json构造并返回一个新的几何对象]</a><br/>
 * <a href='#toJSON'>[14、导出为json对象]</a><br/>
 * [15、克隆几何对象]{@link Geometry#clone}
 *
 * @example <caption><h7 id='Polygon'>不带洞的区几何</h7></caption>
 * // ES5引入方式
 * const { Polygon } = Zondy.Geometry
 * // ES6引入方式
 * import { Polygon } from "@mapgis/webclient-common"
 * new Polygon({
 *   coordinates:[
 *     // 外圈
 *     [
 *       [100.0, 0.0],
 *       [101.0, 0.0],
 *       [101.0, 1.0],
 *       [100.0, 1.0],
 *       [100.0, 0.0]
 *     ]
 *   ]
 * })
 *
 * @example <caption><h7 id='Polygon-innder'>带洞的区几何</h7></caption>
 * // ES5引入方式
 * const { Polygon } = Zondy.Geometry
 * // ES6引入方式
 * import { Polygon } from "@mapgis/webclient-common"
 * new Polygon({
 *   coordinates:[
 *     // 外圈
 *     [
 *       [100.0, 0.0],
 *       [101.0, 0.0],
 *       [101.0, 1.0],
 *       [100.0, 1.0],
 *       [100.0, 0.0]
 *     ],
 *     // 第一个内圈
 *     [
 *       [100.8, 0.8],
 *       [100.8, 0.2],
 *       [100.2, 0.2],
 *       [100.2, 0.8],
 *       [100.8, 0.8]
 *     ],
 *     // 第二个内圈
 *     [],
 *     ...
 *   ]
 * })
 *
 * @example <caption><h7 id='spatialReference'>指定坐标系</h7></caption>
 * // ES5引入方式
 * const { Polygon } = Zondy.Geometry
 * // ES6引入方式
 * import { Polygon } from "@mapgis/webclient-common"
 * new Polygon({
 *   coordinates:[
 *     // 外圈
 *     [
 *       [12060733.232006868, 3377247.5680546067],
 *       [12929863.44711455, 3377247.5680546067],
 *       [12929863.44711455, 3934286.575385226],
 *       [12060733.232006868, 3934286.575385226],
 *       [12060733.232006868, 3377247.5680546067]
 *     ]
 *   ]
 * })
 */
class Polygon extends Geometry {
  constructor(options) {
    super(options)
    options = defaultValue(options, {})
    /**
     * 几何点的坐标
     * @member {Array} Polygon.prototype.coordinates
     */
    this.coordinates = defaultValue(options.coordinates, [])

    this.type = GeometryType.polygon

    if (!Array.isArray(this.coordinates)) {
      throw new Error('坐标必须为数组')
    }
    for (const ring of this.coordinates) {
      if (ring.length < 4) {
        throw new Error('每个环至少有四个点')
      }

      if (ring[ring.length - 1].length !== ring[0].length) {
        throw new Error('首尾点维度不相等')
      }

      for (let j = 0; j < ring[ring.length - 1].length; j++) {
        // Check if first point of Polygon contains two numbers
        if (ring[ring.length - 1][j] !== ring[0][j]) {
          throw new Error('第一个位置和最后一个位置不相等')
        }
      }
    }

    if (this.coordinates[0][0].length === 3) {
      this.hasZ = true
    }
  }

  /**
   * 将给定的Extent转换为多边形实例
   * @param {Extent} extent
   * @return {Polygon} 表示给定范围的多边形实例
   * @example <caption><h7 id='fromExtent'>将给定的Extent转换为多边形实例</h7></caption>
   * // ES5引入方式
   * const { Extent, Polygon } = Zondy.Geometry
   * // ES6引入方式
   * import { Extent, Polygon } from "@mapgis/webclient-common"
   * const extent = new Extent({
   *   "xmin":10,
   *   "xmax":210,
   *   "ymin":0,
   *   "ymax":100
   * })
   * const polygon = Polygon.fromExtent(extent)
   */
  static fromExtent(extent) {
    const coordinates = []
    if (extent instanceof Extent) {
      const ring = [
        [extent.xmin, extent.ymin],
        [extent.xmin, extent.ymax],
        [extent.xmax, extent.ymax],
        [extent.xmax, extent.ymin],
        [extent.xmin, extent.ymin]
      ]
      if (extent && extent.hasZ) {
        const n = extent.zmin + 0.5 * (extent.zmax - extent.zmin)
        ring.forEach((p) => {
          n.push(p)
        })
      }
      coordinates.push(ring)
    }
    return new Polygon({
      coordinates
    })
  }

  /**
   * 在多边形中添加一个环,环可以是一个数字数组或一个点数组
   * @param {Point[]} points 一个多边形环。环中的第一个和最后一个坐标/点必须相同。可以将其定义为Point几何图形数组或XY坐标数组。
   * @return {Polygon} 返回包含新环的多边形
   * @example <caption><h7 id='addRing'>在多边形中添加一个环</h7></caption>
   * // ES5引入方式
   * const { Polygon, Point } = Zondy.Geometry
   * // ES6引入方式
   * import { Polygon, Point } from "@mapgis/webclient-common"
   * const polygon = new Polygon({
   *   coordinates:[
   *     // 外圈
   *     [
   *       [100.0, 0.0],
   *       [101.0, 0.0],
   *       [101.0, 1.0],
   *       [100.0, 1.0],
   *       [100.0, 0.0]
   *     ]
   *   ]
   * })
   * const ring = [
   *   new Point({
   *     coordinates: [100.2, 0.0]
   *   }),
   *   new Point({
   *     coordinates: [101.3, 0.0]
   *   }),
   *   new Point({
   *     coordinates: [100.3, 0.7]
   *   }),
   *   new Point({
   *     coordinates: [104.2, 0.7]
   *   }),
   *   new Point({
   *     coordinates: [100.2, 0.0]
   *   })
   * ]
   * polygon.addRing(ring)
   */
  addRing(points) {
    if (!Array.isArray(points)) throw new Error('传入参数错误')
    const pnts = []
    points.forEach((point) => {
      pnts.push(Point.toCoordinates(point))
    })
    this.coordinates.push(pnts)
    this.coordinates = JSON.parse(JSON.stringify(this.coordinates))
    return this
  }

  /**
   * 根据索引从多边形中移除一个环
   * @param {Number} ringIndex 要移除的环的索引
   * @return {Array}返回表示已移除的环的点数组
   * @example <caption><h7 id='removeRing'>根据索引从多边形中移除一个环</h7></caption>
   * // ES5引入方式
   * const { Polygon } = Zondy.Geometry
   * // ES6引入方式
   * import { Polygon } from "@mapgis/webclient-common"
   * const polygon = new Polygon({
   *   coordinates:[
   *     // 外圈
   *     [
   *       [100.0, 0.0],
   *       [101.0, 0.0],
   *       [101.0, 1.0],
   *       [100.0, 1.0],
   *       [100.0, 0.0]
   *     ]
   *   ]
   * })
   * polygon.removeRing(1)
   */
  removeRing(ringIndex) {
    ringIndex = Number(ringIndex)
    if (!this._isValidate(ringIndex, 0)) return null
    const coords = JSON.parse(
      JSON.stringify(this._cloneCoordinates()[ringIndex])
    )
    this.coordinates.splice(ringIndex, 1)
    this.coordinates = JSON.parse(JSON.stringify(this.coordinates))
    const pointsArray = []
    for (let i = 0; i < coords.length; i++) {
      const point = new Point({
        coordinates: coords[i]
      })
      pointsArray.push(point)
    }
    return pointsArray
  }

  /**
   * 通过传入的json构造并返回一个新的几何对象
   * @param {Object} [json] JSON对象
   * @example <caption><h7 id='fromJSON'>通过传入的json构造并返回一个新的几何对象</h7></caption>
   * // ES5引入方式
   * const { Polygon } = Zondy.Geometry
   * // ES6引入方式
   * import { Polygon } from "@mapgis/webclient-common"
   * const json = {
   *   coordinates:[
   *     // 外圈
   *     [
   *       [100.0, 0.0],
   *       [101.0, 0.0],
   *       [101.0, 1.0],
   *       [100.0, 1.0],
   *       [100.0, 0.0]
   *     ]
   *   ]
   * }
   * const polygon = new Polygon.fromJSON(json)
   */
  static fromJSON(json) {
    json = defaultValue(json, {})
    return new Polygon(json)
  }

  /**
   * <a id='toJSON'></a>
   * 导出为json对象
   * @return {Object} json对象
   */
  toJSON() {
    const json = super.toJSON()
    json.coordinates = JSON.parse(JSON.stringify(this.coordinates))
    json.extent = this.extent.toJSON()
    json.centroid = this.centroid.toJSON()
    return json
  }

  /**
   * 检查输入点是否在多边形内。多边形线上的一个点被认为是内部
   * @param {Point} point 用于测试是否包含在测试多边形中的点
   * @return {Boolean} 如果该点位于多边形内,则返回true
   * @example <caption><h7 id='contains'>检查输入点是否在多边形内</h7></caption>
   * // ES5引入方式
   * const { Polygon, Point } = Zondy.Geometry
   * // ES6引入方式
   * import { Polygon, Point } from "@mapgis/webclient-common"
   * const polygon = new Polygon({
   *   coordinates:[
   *     // 外圈
   *     [
   *       [100.0, 0.0],
   *       [101.0, 0.0],
   *       [101.0, 1.0],
   *       [100.0, 1.0],
   *       [100.0, 0.0]
   *     ]
   *   ]
   * })
   * const point = new Point({
   *   coordinates: [100.0, 0.0]
   * })
   * const isOnPolygon = polygon.contains(point)
   */
  contains(point) {
    if (!point) throw new Error('传入参数错误')
    const polygon = T.polygon(this._cloneCoordinates())
    const p = T.point(Point.toCoordinates(point))
    return T.booleanPointInPolygon(p, polygon)
  }

  /**
   * 根据下标返回指定的点
   * @param {Number} ringIndex 包含所需点的环的下标
   * @param {Number} pointIndex 环内所需点的下标
   * @return {Point | null} 返回位于指定环索引和点索引处的点
   * @example <caption><h7 id='getPoint'>根据下标返回指定的点</h7></caption>
   * // ES5引入方式
   * const { Polygon } = Zondy.Geometry
   * // ES6引入方式
   * import { Polygon } from "@mapgis/webclient-common"
   * const polygon = new Polygon({
   *   coordinates:[
   *     // 外圈
   *     [
   *       [100.0, 0.0],
   *       [101.0, 0.0],
   *       [101.0, 1.0],
   *       [100.0, 1.0],
   *       [100.0, 0.0]
   *     ]
   *   ]
   * })
   * const point = polygon.getPoint(0, 1)
   */
  getPoint(ringIndex, pointIndex) {
    if (!this._isValidate(ringIndex, pointIndex)) return null
    const path = this._cloneCoordinates()[ringIndex]
    return new Point({
      coordinates: path[pointIndex]
    })
  }

  /**
   * 在多边形中插入一个新点
   * @param {Number} ringIndex 插入点的环的下标。
   * @param {Number} pointIndex 要插入环内的点的下标
   * @param {Point} point 插入点
   * @return {Polygon | null} 返回更新后的多边形
   * @example <caption><h7 id='insertPoint'>在多边形中插入一个新点</h7></caption>
   * // ES5引入方式
   * const { Polygon, Point } = Zondy.Geometry
   * // ES6引入方式
   * import { Polygon, Point } from "@mapgis/webclient-common"
   * const polygon = new Polygon({
   *   coordinates:[
   *     // 外圈
   *     [
   *       [100.0, 0.0],
   *       [101.0, 0.0],
   *       [101.0, 1.0],
   *       [100.0, 1.0],
   *       [100.0, 0.0]
   *     ]
   *   ]
   * })
   * const point = new Point({
   *   coordinates: [99, 1.0],
   * })
   * polygon.insertPoint(0, 3, point)
   */
  insertPoint(ringIndex, pointIndex, point) {
    if (!this._isValidate(ringIndex, pointIndex)) return null
    const path = this.coordinates[ringIndex]
    if (Array.isArray(path)) {
      path.splice(pointIndex, 0, point._cloneCoordinates())
    }
    this.coordinates[ringIndex] = path
    this.coordinates = JSON.parse(JSON.stringify(this.coordinates))
    return this
  }

  /**
   * 根据下标从多边形中移除一个点
   * @param {Number} ringIndex 包含要移除的点的环的下标。
   * @param {Number} pointIndex 要在环内移除的点的下标。
   * @return {Point|null} 返回被移除点
   * @example <caption><h7 id='removePoint'>根据下标从多边形中移除一个点</h7></caption>
   * // ES5引入方式
   * const { Polygon } = Zondy.Geometry
   * // ES6引入方式
   * import { Polygon } from "@mapgis/webclient-common"
   * const polygon = new Polygon({
   *   coordinates:[
   *     // 外圈
   *     [
   *       [100.0, 0.0],
   *       [101.0, 0.0],
   *       [101.0, 1.0],
   *       [100.0, 1.0],
   *       [100.0, 0.0]
   *     ]
   *   ]
   * })
   * polygon.removePoint(0, 1)
   */
  removePoint(ringIndex, pointIndex) {
    if (!this._isValidate(ringIndex, pointIndex)) return null
    const _polygon = this.clone()
    const path = _polygon.coordinates[ringIndex]
    const point = new Point({
      coordinates: path[pointIndex]
    })
    if (Array.isArray(path)) {
      path.splice(pointIndex, 1)
    }
    this.coordinates = _polygon.coordinates
    this.coordinates = JSON.parse(JSON.stringify(this.coordinates))
    return point
  }

  /**
   * 更新多边形中的一个点
   * @param {Number} ringIndex 包含要更新点的环的索引。
   * @param {Number} pointIndex 在环内要更新的点的索引。
   * @param {Point|Number} point 新的点
   * @return {Polygon} 返回更新后的多边形
   * @example <caption><h7 id='setPoint'>更新多边形中的一个点</h7></caption>
   * // ES5引入方式
   * const { Polygon, Point } = Zondy.Geometry
   * // ES6引入方式
   * import { Polygon, Point } from "@mapgis/webclient-common"
   * const polygon = new Polygon({
   *   coordinates:[
   *     // 外圈
   *     [
   *       [100.0, 0.0],
   *       [101.0, 0.0],
   *       [101.0, 1.0],
   *       [100.0, 1.0],
   *       [100.0, 0.0]
   *     ]
   *   ]
   * })
   * const point = new Point({
   *   coordinates: [99, 1.0],
   * })
   * polygon.setPoint(0, 3, point)
   */
  setPoint(ringIndex, pointIndex, point) {
    if (!this._isValidate(ringIndex, pointIndex)) return this
    if (point instanceof Point) {
      point = point._cloneCoordinates()
    }
    const path = this.coordinates[ringIndex]
    if (Array.isArray(path)) {
      path.splice(pointIndex, 1, point)
    }
    this.coordinates[ringIndex] = path
    this.coordinates = JSON.parse(JSON.stringify(this.coordinates))
    return this
  }

  /**
   * 导出为OGC服务要求的xml字符串<a id='toXMl'></a>
   * @return {String} 字符串
   */
  toXMl() {
    return ''
  }

  /**
   * 返回如下格式的字符串:"x0,y0,x1,y1,x2,y2,x0,y0|x3,y3,x4,y4,x5,y5,x3,y3"<a id='toString'></a>
   * 多边形内每个的多边形由|号分割
   * @returns string
   */
  toString() {
    let _str = ''
    // 开始循环多边形内的多边形
    for (let i = 0; i < this.coordinates.length; i++) {
      // 内部多边形,第一个是多边形本身
      const _innerPolygon = this.coordinates[i]
      // 开始循环内多边形里的点
      for (let j = 0; j < _innerPolygon.length; j++) {
        // 多边形里的点
        const _point = _innerPolygon[j]
        // 有可能是xyz,因此for循环
        for (let k = 0; k < _point.length; k++) {
          // 加,号
          _str += `${_point[k]},`
        }
      }
      // 删除最后一个,
      _str = _str.substring(0, _str.length - 1)
      // 加上|号
      _str += '|'
    }
    // 删除最后一个|
    _str = _str.substring(0, _str.length - 1)
    return _str
  }

  /**
   * 返回igs1.0的几何对象<a id='toOldIGSGeometry'></a>
   * @returns Object igs1.0的几何对象
   */
  toOldIGSGeometry() {
    const RegGeom = []
    const reg = {
      Rings: [],
      GID: 0
    }
    let ring = {}
    const polygon = this.coordinates
    for (let i = 0; i < polygon.length; i++) {
      ring = {
        Arcs: []
      }
      const Dots = []
      for (let j = 0; j < polygon[i].length; j++) {
        const ringCos = polygon[i][j]
        Dots.push({
          x: ringCos[0],
          y: ringCos[1]
        })
      }
      ring.Arcs.push({
        Dots,
        ArcID: i
      })
      reg.Rings.push(ring)
    }
    RegGeom.push(reg)
    return {
      RegGeom
    }
  }

  /**
   * 返回IGS所对应的GeometryModule型<a id='getIGSType'></a>
   * @returns string GeometryModule型
   */
  getIGSType() {
    return IGSGeometryType.polygon
  }

  /**
   * 返回Dots对象,仅包括多边形的外圈<a id='toDots'></a>
   * @returns Array Dots对象
   */
  toDots() {
    const Dots = []
    const coordinates = this.coordinates[0]

    for (let i = 0; i < coordinates.length; i++) {
      Dots.push({
        x: coordinates[i][0],
        y: coordinates[i][1]
      })
    }

    return Dots
  }

  /**
   * 多边形的环是否是顺时针
   * @param {Array | Array<Point>} ring 多边形的环,坐标数组或者点几何数组
   * @return {Boolean} 是否是顺时针
   * */
  isClockwise(ring) {
    let isClockwise = false
    const points = []

    if (!(ring instanceof Array)) {
      throw new Error('ring不是数组类型!')
    }

    for (let i = 0; i < ring.length; i++) {
      if (ring[i] instanceof Point) {
        points.push(ring[i].coordinates)
      } else if (ring[i] instanceof Array && ring[i].length >= 2) {
        points.push(ring[i])
      } else {
        throw new Error('ring中的点不是数组或Point类型!')
      }
    }
    const line = T.lineString(points)
    isClockwise = T.booleanClockwise(line)

    return isClockwise
  }

  /**
   * @function Polygon.prototype._isValidate
   * @private
   * @description 判断输入是否合法
   * @param {Number} ringIndex 环下标
   * @param {Number} pointIndex 数组下标
   * @returns {Boolean} 输入是否合法
   */
  _isValidate(ringIndex, pointIndex) {
    const len = this.coordinates.length
    if (!isNumber(ringIndex) || ringIndex < 0 || ringIndex >= len) return false
    const path = this.coordinates[ringIndex]
    const pathLen = path.length
    if (!isNumber(pointIndex) || pointIndex < 0 || pointIndex >= pathLen) {
      return false
    }
    return true
  }

  /**
   * @function Polygon.prototype._cloneCoordinates
   * @private
   * @description 拷贝几何信息
   * @returns {Array}
   */
  _cloneCoordinates() {
    return JSON.parse(JSON.stringify(this.coordinates))
  }

  /**
   * @function Polygon.prototype._calcExtent
   * @private
   * @description 计算外包盒
   * @param {Array} coordinates 坐标点
   * @param {Boolean} hasZ 是否是三维
   * @returns {Extent}
   */
  _calcExtent(coordinates, hasZ) {
    return calcExtent(coordinates, hasZ)
  }

  /**
   * 判断多边形内部是否自相交
   * @private
   * */
  _isSelfIntersecting() {
    const polygon = T.polygon(this.coordinates)
    const kinks = T.kinks(polygon)
    if (kinks.features.length > 0) {
      return true
    }
    return false
  }

  /**
   * 设置多边形质心
   * @private
   * */
  _setCentroid() {
    const polygon = T.polygon(this.coordinates)
    const centroid = T.centroid(polygon)

    // 计算高度平均值
    if (this.hasZ) {
      let length = 0
      let height = 0
      for (let i = 0; i < this.coordinates.length; i++) {
        length += this.coordinates[i].length
        for (let j = 0; j < this.coordinates[i].length; j++) {
          height += this.coordinates[i][j][2]
        }
      }
      height /= length
      return new Point({
        coordinates: [
          centroid.geometry.coordinates[0],
          centroid.geometry.coordinates[1],
          height
        ]
      })
    } else {
      return new Point({
        coordinates: [
          centroid.geometry.coordinates[0],
          centroid.geometry.coordinates[1]
        ]
      })
    }
  }

  /**
   * 克隆几何对象
   * @return {Geometry} 克隆后的几何对象
   */
  clone() {
    return new Polygon(this.toJSON())
  }
}
Object.defineProperties(Polygon.prototype, {
  /**
   * 外包盒
   * @member {Number} Polygon.prototype.extent
   * */
  extent: {
    configurable: false,
    get() {
      return this._calcExtent(this.coordinates, this.hasZ)
    }
  },
  /**
   * 多边形是否自相交;判断多边形内的环是否互相相交;或多边形内部的环是否自相交
   * @member {Boolean} Polygon.prototype.isSelfIntersecting
   * @default false
   */
  isSelfIntersecting: {
    configurable: false,
    get() {
      return this._isSelfIntersecting()
    }
  },
  /**
   * 多边形质心
   * @member {Point} Polygon.prototype.centroid
   */
  centroid: {
    configurable: false,
    get() {
      return this._setCentroid()
    }
  }
})
Zondy.Geometry.Polygon = Polygon
export default Polygon
构造函数
成员变量
方法
事件