类名 common/base/geometry/Extent.js
import * as T from '@turf/turf'
import Geometry from './Geometry'
import {defaultValue, Log, isNumber, toJSON} from '../../util'
import Zondy from '../Zondy'
import Point from './Point'
import { GeometryType, IGSGeometryType } from '../enum'
import Projection from '../Projection'
import SpatialReference from './SpatialReference'
import GeometryEngine from './GeometryEngine'

/**
 * 几何范围对象,即左下角和右上角组成的矩形范围几何对象,参考示例:<a href='#Extent'>[几何范围对象]</a>
 * <br><br>[ES5引入方式]:<br/>
 * Zondy.Geometry.Extent() <br/>
 * [ES6引入方式]:<br/>
 * import { Extent } from "@mapgis/webclient-common" <br/>
 * <br/>
 * @class Extent
 * @moduleEX GeometryModule
 * @extends Geometry
 * @param {Object} options 构造参数
 * @param {Number} [options.xmin = 0] x轴最小坐标
 * @param {Number} [options.ymin = 0] y轴最小坐标
 * @param {Number} [options.xmax = 0] x轴最大坐标
 * @param {Number} [options.ymax = 0] y轴最大坐标
 * @param {Number} [options.zmin0] Z轴最小坐标
 * @param {Number} [options.zmax0] Z轴最大坐标
 * @param {SpatialReference} [options.spatialReference = new Zondy.SpatialReference('EPSG:4326')] 几何点的空间参考系,默认4326,当不是4326时请指定坐标系,方便进行投影转换,参考示例:<a href='#SpatialReference'>[指定坐标系]</a>
 *
 * @summary <h5>支持如下方法:</h5>
 * <a href='#centerAt'>[1、根据中心点生成新的范围]</a><br/>
 * <a href='#contains'>[2、输入的几何是否包含在范围内]</a><br/>
 * <a href='#intersection'>[3、将原始范围与输入范围求交]</a><br/>
 * <a href='#intersects'>[4、输入的几何图形是否与范围相交]</a><br/>
 * <a href='#equals'>[5、是否和输入范围相等]</a><br/>
 * <a href='#expand'>[6、按给定的因子扩大范围]</a><br/>
 * <a href='#offset'>[7、平移extent]</a><br/>
 * <a href='#union'>[8、求并]</a><br/>
 * <a href='#toString'>[9、返回字符串]</a><br/>
 * <a href='#toIGSOldString'>[10、返回IGS1.0的字符串]</a><br/>
 * <a href='#getIGSType'>[11、返回IGS所对应的GeometryModule型]</a><br/>
 * <a href='#fromJSON'>[12、通过传入的json构造并返回一个新的几何对象]</a><br/>
 * <a href='#toJSON'>[13、导出为json对象]</a><br/>
 * [14、克隆几何对象]{@link Geometry#clone}
 *
 * @example <caption><h7 id='Extent'>创建几何对象</h7></caption>
 * // ES5引入方式
 * const { Extent } = Zondy.Geometry
 * // ES6引入方式
 * import { Extent } from "@mapgis/webclient-common"
 * new Extent({
 *   xmin: 10,
 *   xmax: 210,
 *   ymin: 0,
 *   ymax: 100
 * })
 *
 * @example <caption><h7 id='spatialReference'>指定坐标系</h7></caption>
 * // ES5引入方式
 * const { Extent } = Zondy.Geometry
 * const { SpatialReference } = Zondy
 * // ES6引入方式
 * import { Extent, SpatialReference } from "@mapgis/webclient-common"
 * new Extent({
 *   // 3857坐标系的点
 *   xmin: 12060733.232006868,
 *   xmax: 12929863.44711455,
 *   ymin: 3377247.5680546067,
 *   ymax: 3934286.575385226,
 *   // 当不是4326时请指定坐标系,方便进行投影转换
 *   spatialReference: new SpatialReference('EPSG:3857')
 * })
 */

class Extent extends Geometry {
  constructor(options) {
    super(options)

    options = defaultValue(options, {})
    /**
     * x坐标最小值
     * @default 0
     * @member {Number} Extent.prototype.xmin
     */
    this.xmin = defaultValue(options.xmin, 0)
    /**
     * y坐标最小值
     * @default 0
     * @member {Number} Extent.prototype.ymin
     */
    this.ymin = defaultValue(options.ymin, 0)
    /**
     * x坐标最大值
     * @default 0
     * @member {Number} Extent.prototype.xmax
     */
    this.xmax = defaultValue(options.xmax, 0)
    /**
     * y坐标最大值
     * @default 0
     * @member {Number} Extent.prototype.ymax
     */
    this.ymax = defaultValue(options.ymax, 0)
    /**
     * z坐标最小值
     * @member {Number} Extent.prototype.zmin
     */
    this.zmin = defaultValue(options.zmin, undefined)
    /**
     * z坐标最大值
     * @member {Number} Extent.prototype.zmax
     */
    this.zmax = defaultValue(options.zmax, undefined)
    this.type = GeometryType.extent
    // 判断hasZ
    this.hasZ = !(!isNumber(this.zmin) || !isNumber(this.zmax))
    // 判断坐标是否为数字
    if (
      !isNumber(this.xmin) ||
      !isNumber(this.ymin) ||
      !isNumber(this.xmax) ||
      !isNumber(this.ymax) ||
      (typeof this.zmin !== 'undefined' && !isNumber(this.zmin)) ||
      (typeof this.zmax !== 'undefined' && !isNumber(this.zmax))
    ) {
      throw new Error('坐标必须为数字')
    }
  }

  /**
   * 根据中心点生成新的范围,新范围的宽高为当前范围的宽高
   * @param {Point} point 中心点
   * @returns {Extent} 以点为中心的新范围。
   * @example <caption><h7 id='centerAt'>根据中心点生成新的范围</h7></caption>
   * // ES5引入方式
   * const { Extent, Point } = Zondy.Geometry
   * // ES6引入方式
   * import { Extent, Point } from "@mapgis/webclient-common"
   * const extent = new Extent({
   *   xmin: 10,
   *   xmax: 210,
   *   ymin: 0,
   *   ymax: 100
   * })
   * const point = new Point({
   *   coordinates: [100.0, 0.0]
   * })
   * const newConst = extent.centerAt(point)
   */
  centerAt(point) {
    if (!point) throw new Error('传入参数错误')
    // 对传入参数为Number[]或者Point的处理
    const coordinates = Point.toCoordinates(point)
    const { width } = this
    const { height } = this
    this.xmin = coordinates[0] - width / 2
    this.xmax = coordinates[0] + width / 2
    this.ymin = coordinates[1] - height / 2
    this.ymax = coordinates[1] + height / 2
    if (this.hasZ && point.hasZ) {
      const zlength = this.zmax - this.zmin
      this.zmin = coordinates[2] - zlength / 2
      this.zmax = coordinates[2] + zlength / 2
    }
    return this
  }

  /**
   * 输入的几何是否包含在范围内
   * @param {Point|Extent} geometry 输入的几何图形
   * @returns {Boolean} 如果输入几何图形包含在范围内,则返回true
   * @example <caption><h7 id='contains'>输入的几何是否包含在范围内</h7></caption>
   * // ES5引入方式
   * const { Extent, Point } = Zondy.Geometry
   * // ES6引入方式
   * import { Extent, Point } from "@mapgis/webclient-common"
   * const extent = new Extent({
   *   xmin: 10,
   *   xmax: 210,
   *   ymin: 0,
   *   ymax: 100
   * })
   * const point = new Point({
   *   coordinates: [100.0, 0.0]
   * })
   * const isIn = extent.contains(point)
   */
  contains(geometry) {
    // 使用turf判断面与面,面与点的包含关系
    if (!(geometry instanceof Point) && !(geometry instanceof Extent)) {
      return false
    }

    // 因为turf的精度问题,将几何转为4326进行比较
    let _geometry = geometry
    if (!_geometry.isGeographic) {
      _geometry = Projection.project(
        geometry,
        new SpatialReference({ wkid: 4326 })
      )
    }
    let _extent = this
    if (!_extent.isGeographic) {
      _extent = Projection.project(this, new SpatialReference({ wkid: 4326 }))
    }

    const extentPolygonOri = T.polygon([
      [
        [_extent.xmin, _extent.ymin],
        [_extent.xmin, _extent.ymax],
        [_extent.xmax, _extent.ymax],
        [_extent.xmax, _extent.ymin],
        [_extent.xmin, _extent.ymin]
      ]
    ])
    let currentGeometry
    if (_geometry instanceof Point) {
      currentGeometry = T.point(_geometry._cloneCoordinates())
    } else {
      currentGeometry = T.polygon([
        [
          [_geometry.xmin, _geometry.ymin],
          [_geometry.xmin, _geometry.ymax],
          [_geometry.xmax, _geometry.ymax],
          [_geometry.xmax, _geometry.ymin],
          [_geometry.xmin, _geometry.ymin]
        ]
      ])
    }
    return T.booleanContains(extentPolygonOri, currentGeometry)
  }

  /**
   * 将原始范围与输入范围求交
   * @param {Extent}  extent 输入的范围,用于相交。
   * @returns {Extent} 返回输入Extent与原始Extent的交集,没有交集返回 null
   * @example <caption><h7 id='intersection'>将原始范围与输入范围求交</h7></caption>
   * // ES5引入方式
   * const { Extent } = Zondy.Geometry
   * // ES6引入方式
   * import { Extent } from "@mapgis/webclient-common"
   * const extent = new Extent({
   *   xmin: 10,
   *   xmax: 210,
   *   ymin: 0,
   *   ymax: 100
   * })
   * const extent2 = new Extent({
   *   xmin: 10,
   *   xmax: 210,
   *   ymin: 0,
   *   ymax: 100
   * })
   * const newExtent = extent.intersection(extent2)
   */
  intersection(extent) {
    if (!extent) Log.error('传入参数错误!')
    if (!(extent instanceof Extent)) Log.error('extent不是Extent几何!')
    if (!this.spatialReference.equals(extent.spatialReference)) {
      Log.error('不是同一个坐标系!')
    }

    // 因为turf的精度问题,将几何转为4326进行比较
    let _geometry = extent
    if (!_geometry.isGeographic) {
      _geometry = Projection.project(
        extent,
        new SpatialReference({ wkid: 4326 })
      )
    }
    let _extent = this
    if (!_extent.isGeographic) {
      _extent = Projection.project(this, new SpatialReference({ wkid: 4326 }))
    }

    const extentPolygonOri = T.polygon([
      [
        [_extent.xmin, _extent.ymin],
        [_extent.xmin, _extent.ymax],
        [_extent.xmax, _extent.ymax],
        [_extent.xmax, _extent.ymin],
        [_extent.xmin, _extent.ymin]
      ]
    ])
    const extentPolygonAdd = T.polygon([
      [
        [_geometry.xmin, _geometry.ymin],
        [_geometry.xmin, _geometry.ymax],
        [_geometry.xmax, _geometry.ymax],
        [_geometry.xmax, _geometry.ymin],
        [_geometry.xmin, _geometry.ymin]
      ]
    ])

    let result = null
    const geoJSON = T.intersect(extentPolygonOri, extentPolygonAdd)
    if (geoJSON && geoJSON.geometry) {
      let bbox
      if (geoJSON.geometry.type === 'Polygon') {
        bbox = T.bbox(T.polygon(geoJSON.geometry.coordinates))
      } else if (geoJSON.geometry.type === 'MultiPolygon') {
        bbox = T.bbox(T.multiPolygon(geoJSON.geometry.coordinates))
      }
      if (bbox) {
        result = new Extent({
          xmin: bbox[0],
          ymin: bbox[1],
          xmax: bbox[2],
          ymax: bbox[3]
        })
        if (!this.spatialReference.isGeographic) {
          result = Projection.project(
            result,
            SpatialReference.fromJSON(this.spatialReference)
          )
        }
      }
    }

    return result
  }

  /**
   * 输入的几何图形是否与范围相交
   * @param {Geometry}  geometry  用于测试相交的几何图形。可以是 Point, Polyline, Polygon, Extent or Multipoint
   * @returns {Boolean} 如果输入几何图形与区段相交,则返回true
   * @example <caption><h7 id='intersects'>输入的几何图形是否与范围相交</h7></caption>
   * // ES5引入方式
   * const { Extent, Point } = Zondy.Geometry
   * // ES6引入方式
   * import { Extent, Point } from "@mapgis/webclient-common"
   * const extent = new Extent({
   *   xmin: 10,
   *   xmax: 210,
   *   ymin: 0,
   *   ymax: 100
   * })
   * const point = new Point({
   *   coordinates: [100.0, 0.0]
   * })
   * const isIntersect = extent.intersects(point)
   */
  intersects(geometry) {
    return GeometryEngine.intersects(this, geometry)
  }

  /**
   * 是否和输入范围相等
   * @param {Extent} extent 输入的外包范围
   * @returns {Boolean} 如果输入范围等于调用equals()的范围,则返回true
   * @example <caption><h7 id='equals'>是否和输入范围相等</h7></caption>
   * // ES5引入方式
   * const { Extent } = Zondy.Geometry
   * // ES6引入方式
   * import { Extent } from "@mapgis/webclient-common"
   * const extent = new Extent({
   *   xmin: 10,
   *   xmax: 210,
   *   ymin: 0,
   *   ymax: 100
   * })
   * const extent2 = new Extent({
   *   xmin: 10,
   *   xmax: 210,
   *   ymin: 0,
   *   ymax: 100
   * })
   * const isEqual = extent.equals(extent2)
   */
  equals(extent) {
    if (!extent) throw new Error('传入参数错误')
    const condition1 = !!Math.abs(this.xmax - extent.xmax) <= 10e-8
    const condition2 = !!Math.abs(this.xmin - extent.xmin) <= 10e-8
    const condition3 = !!Math.abs(this.ymax - extent.ymax) <= 10e-8
    const condition4 = !!Math.abs(this.ymin - extent.ymin) <= 10e-8
    if (!this.hasZ) {
      return condition1 && condition2 && condition3 && condition4
    }
    const condition5 = !!Math.abs(this.zmax - extent.zmax) <= 10e-8
    const condition6 = !!Math.abs(this.zmin - extent.zmin) <= 10e-8
    return (
      condition1 &&
      condition2 &&
      condition3 &&
      condition4 &&
      condition5 &&
      condition6
    )
  }

  /**
   * 按给定的因子扩大范围。例如,值为1.5将将范围扩展到比原始范围大50%。
   * @param {Number}  factor 乘数的值,必须大于等于1
   * @returns {Extent} 返回扩大的范围
   * @example <caption><h7 id='expand'>按给定的因子扩大范围</h7></caption>
   * // ES5引入方式
   * const { Extent } = Zondy.Geometry
   * // ES6引入方式
   * import { Extent } from "@mapgis/webclient-common"
   * const extent = new Extent({
   *   xmin: 10,
   *   xmax: 210,
   *   ymin: 0,
   *   ymax: 100
   * })
   * extent.expand(1.5)
   */
  expand(factor) {
    if (typeof factor === 'number') {
      if (factor >= 1) {
        factor = Math.abs(factor)
        const width = this.width * factor
        const height = this.height * factor
        const widthSub = width - this.width
        const heightSub = height - this.height
        this.xmin -= widthSub / 2
        this.xmax += widthSub / 2
        this.ymin -= heightSub / 2
        this.ymax += heightSub / 2
      }
      return this
    } else {
      throw new Error('按照给定的因子扩大范围,需要的是一个num值')
    }
  }

  /**
   * 根据输入的dx, dy, dz值,平移extent
   * @param {Number}  dx 要平移的x值
   * @param {Number}  dy 要平移的y值
   * @param {Number}  dz 要平移的z值
   * @returns {Extent} 偏移后的范围
   * @example <caption><h7 id='offset'>平移extent</h7></caption>
   * // ES5引入方式
   * const { Extent } = Zondy.Geometry
   * // ES6引入方式
   * import { Extent } from "@mapgis/webclient-common"
   * const extent = new Extent({
   *   xmin: 10,
   *   xmax: 210,
   *   ymin: 0,
   *   ymax: 100
   * })
   * extent.offset(0, 1, 0)
   */
  offset(dx, dy, dz) {
    if (!isNumber(dx) || !isNumber(dy) || (this.hasZ && !isNumber(dz))) {
      throw new Error('传入参数错误')
    }
    this.xmax += dx
    this.xmin += dx
    this.ymax += dy
    this.ymin += dy
    if (this.hasZ) {
      this.zmax += dz
      this.zmin += dz
    }
    return this
  }

  /**
   * 求并
   * @param {Extent}  extent 输入的范围,用于并集
   * @returns {Extent} 返回输入Extent与原始Extent的并集
   * @example <caption><h7 id='union'>求并</h7></caption>
   * // ES5引入方式
   * const { Extent } = Zondy.Geometry
   * // ES6引入方式
   * import { Extent } from "@mapgis/webclient-common"
   * const extent = new Extent({
   *   xmin: 10,
   *   xmax: 210,
   *   ymin: 0,
   *   ymax: 100
   * })
   * const extent2 = new Extent({
   *   xmin: 10,
   *   xmax: 210,
   *   ymin: 0,
   *   ymax: 100
   * })
   * const newExtent = extent.union(extent2)
   */
  union(extent) {
    if (!extent) throw new Error('传入参数错误')
    if (!this.hasZ) {
      return new Extent({
        xmin: extent.xmin < this.xmin ? extent.xmin : this.xmin,
        xmax: extent.xmax > this.xmax ? extent.xmax : this.xmax,
        ymin: extent.ymin < this.ymin ? extent.ymin : this.ymin,
        ymax: extent.ymax > this.ymax ? extent.ymax : this.ymax
      })
    }
    return new Extent({
      xmin: extent.xmin < this.xmin ? extent.xmin : this.xmin,
      xmax: extent.xmax > this.xmax ? extent.xmax : this.xmax,
      ymin: extent.ymin < this.ymin ? extent.ymin : this.ymin,
      ymax: extent.ymax > this.ymax ? extent.ymax : this.ymax,
      zmin: extent.zmin < this.zmin ? extent.zmin : this.zmin,
      zmax: extent.zmax > this.zmax ? extent.zmax : this.zmax
    })
  }

  /**
   * 返回如下格式的字符串:"xmin,ymin,xmax,ymax"<a id='toString'></a>
   * @returns string
   */
  toString() {
    if (this.hasZ) {
      return `${this.xmin},${this.ymin},${this.zmin},${this.xmax},${this.ymax},${this.zmax}`
    }
    return `${this.xmin},${this.ymin},${this.xmax},${this.ymax}`
  }

  /**
   * 通过传入的json构造并返回一个新的几何对象
   * @param {Object} [json] JSON对象
   * @example <caption><h7 id='fromJSON'>通过传入的json构造并返回一个新的几何对象</h7></caption>
   * // ES5引入方式
   * const { Extent } = Zondy.Geometry
   * // ES6引入方式
   * import { Extent } from "@mapgis/webclient-common"
   * const json = {
   *   xmin: 10,
   *   xmax: 210,
   *   ymin: 0,
   *   ymax: 100
   * }
   * const extent = Extent.fromJSON(json)
   */
  static fromJSON(json) {
    json = defaultValue(json, {})
    return new Extent(json)
  }

  /**
   * <a id='toJSON'></a>
   * 导出为json对象
   * @return {Object} json对象
   */
  toJSON() {
    const json = super.toJSON()
    json.xmin = this.xmin
    json.ymin = this.ymin
    json.xmax = this.xmax
    json.ymax = this.ymax
    json.zmin = this.zmin
    json.zmax = this.zmax
    json.width = this.width
    json.height = this.height

    return json
  }

  /**
   * 返回如下格式的字符串:"xmin$ymin$xmax$ymax"<a id='toIGSOldString'></a>
   * @returns string
   */
  toIGSOldString() {
    return `${this.xmin}$${this.ymin}$${this.xmax}$${this.ymax}`
  }

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

  /**
   * 归一化计算
   * @returns {Extent} 归一化后的点对象
   * @example <caption><h7 id='normalize'>归一化计算</h7></caption>
   * // ES5引入方式
   * const { Extent } = Zondy.Geometry
   * // ES6引入方式
   * import { Extent } from "@mapgis/webclient-common"
   * const extent = new Extent({
   *   xmin: 10,
   *   xmax: 210,
   *   ymin: 0,
   *   ymax: 100
   * })
   * const normalize = extent.normalize()
   */
  normalize() {
    let pointMin = new Point({
      coordinates: [this.xmin, this.ymin]
    })
    let pointMax = new Point({
      coordinates: [this.xmax, this.ymax]
    })

    pointMin = pointMin.normalize()
    pointMax = pointMax.normalize()
    const extent = new Extent({
      xmin: pointMin.coordinates[0],
      ymin: pointMin.coordinates[1],
      xmax: pointMax.coordinates[0],
      ymax: pointMax.coordinates[1]
    })

    return extent
  }

  /**
   * 克隆几何对象
   * @return {Geometry} 克隆后的几何对象
   */
  clone() {
    return new Extent(this.toJSON())
  }
}

Object.defineProperties(Extent.prototype, {
  /**
   * 宽度
   * @member {Number} Extent.prototype.width
   * */
  width: {
    configurable: false,
    get() {
      return this.xmax - this.xmin
    }
  },
  /**
   * 高度
   * @member {Number} Extent.prototype.height
   * */
  height: {
    configurable: false,
    get() {
      return this.ymax - this.ymin
    }
  },
  /**
   * 中心点
   * @member {Number[]} Extent.prototype.center
   * */
  center: {
    configurable: false,
    set(center) {
      this.centerAt(center)
    },
    get() {
      if (!this.hasZ) {
        return new Point({
          coordinates: [
            (this.xmax - this.xmin) / 2 + this.xmin,
            (this.ymax - this.ymin) / 2 + this.ymin
          ]
        })
      }
      return new Point({
        coordinates: [
          (this.xmax - this.xmin) / 2 + this.xmin,
          (this.ymax - this.ymin) / 2 + this.ymin,
          (this.zmax - this.zmin) / 2 + this.zmin
        ]
      })
    }
  }
})

Zondy.Geometry.Extent = Extent
export default Extent
构造函数
成员变量
方法
事件