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