1.添加森林防火地图模板页的初版;2.添加工具栏,集成绘制位置、线、面、曲面、直箭头、宽箭头、攻击箭头、双箭头、测距、测面功能;3.添加清除功能,删除所有绘制、测量的地图要素;4.添加仓库、水源地的绘制;5.实现属性添加、编辑机制;6.添加弹窗,可以显示和修改当前新增、点击的地图要素属性;
|
@ -13,6 +13,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "2.3.1",
|
||||
"@turf/turf": "^7.2.0",
|
||||
"@vueup/vue-quill": "1.2.0",
|
||||
"@vueuse/core": "10.11.0",
|
||||
"axios": "0.28.1",
|
||||
|
@ -26,6 +27,7 @@
|
|||
"js-cookie": "3.0.5",
|
||||
"jsencrypt": "3.3.2",
|
||||
"json-editor-vue": "^0.18.1",
|
||||
"mitt": "^3.0.1",
|
||||
"nprogress": "0.2.0",
|
||||
"pinia": "2.1.7",
|
||||
"splitpanes": "3.1.5",
|
||||
|
|
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 8.0 KiB |
After Width: | Height: | Size: 864 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1020 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1010 B |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 639 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 849 B |
After Width: | Height: | Size: 753 B |
After Width: | Height: | Size: 822 B |
After Width: | Height: | Size: 839 B |
After Width: | Height: | Size: 1.1 KiB |
|
@ -63,6 +63,10 @@ export function useConfigSetting(viewer) {
|
|||
|
||||
// 地图设置视角
|
||||
const mapSetView = (options) => {
|
||||
if (!options.setView) {
|
||||
return;
|
||||
}
|
||||
|
||||
let destination = Cesium.Cartesian3.fromDegrees(options.setView.x, options.setView.y, options.setView.z);
|
||||
let orientation = {
|
||||
heading: Cesium.Math.toRadians(options.setView.heading),
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
import * as Cesium from 'cesium'
|
||||
import DrawUtil from './drawUtil'
|
||||
import { createAttackStraightArrowPositions } from './attackStraightArrowGraphic'
|
||||
|
||||
// const tailWidthFactor = 0.07
|
||||
const neckWidthFactor = 0.07
|
||||
const headWidthFactor = 0.1
|
||||
const headAngle = Math.PI - Math.PI / 8.5
|
||||
const neckAngle = Math.PI - Math.PI / 13
|
||||
|
||||
// 创建箭头点数组
|
||||
export function createAttackArrowPositions(worldPositions) {
|
||||
if (!worldPositions || worldPositions.length < 3) {
|
||||
return []
|
||||
} else if (worldPositions.length === 3) {
|
||||
return createAttackStraightArrowPositions(worldPositions)
|
||||
}
|
||||
|
||||
let pnts = worldPositions.map((car3) => {
|
||||
const carto = Cesium.Cartographic.fromCartesian(car3)
|
||||
return [Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude)]
|
||||
})
|
||||
// 箭头尾部中间
|
||||
let pntTail = DrawUtil.mid(pnts[0], pnts[1])
|
||||
// 箭头顶点
|
||||
let pntHead = pnts[pnts.length - 1]
|
||||
// 靠近尾点的点
|
||||
let pntTailNear = pnts[1]
|
||||
// 靠近顶点的点
|
||||
let pntNeck = pnts[pnts.length - 2]
|
||||
|
||||
let len = DrawUtil.getBaseLength(pnts)
|
||||
// let tailWidth = len * tailWidthFactor
|
||||
// let neckWidth = len * neckWidthFactor
|
||||
// let headWidth = len * headWidthFactor
|
||||
const tailWidth = DrawUtil.getBaseLength(pnts.slice(0, 2)) / 2
|
||||
const neckWidth = tailWidth
|
||||
const headWidth = (tailWidth * 10) / 7
|
||||
const isClockWise = DrawUtil.isClockWise(pnts[0], pnts[1], pnts[2])
|
||||
// 箭头左尾点
|
||||
let tailLeft = isClockWise ? pnts[0] : pnts[1]
|
||||
// 箭头右尾点
|
||||
let tailRight = isClockWise ? pnts[1] : pnts[0]
|
||||
// 箭头左端点
|
||||
let headLeft = DrawUtil.getThirdPoint(pntNeck, pntHead, headAngle, headWidth, false)
|
||||
// 箭头右端点
|
||||
let headRight = DrawUtil.getThirdPoint(pntNeck, pntHead, headAngle, headWidth, true)
|
||||
// 箭头左颈点
|
||||
let neckLeft = DrawUtil.getThirdPoint(pntNeck, pntHead, neckAngle, neckWidth, false)
|
||||
// 箭头右颈点
|
||||
let neckRight = DrawUtil.getThirdPoint(pntNeck, pntHead, neckAngle, neckWidth, true)
|
||||
|
||||
// 生成样条曲线
|
||||
const innerPoints = pnts.slice(2, pnts.length - 1)
|
||||
const minHalfWidth = neckWidth * Math.sin(Math.PI / 13)
|
||||
const leftControls = [
|
||||
tailLeft,
|
||||
...DrawUtil.getArrowAsideControlPoints(
|
||||
[tailLeft, ...innerPoints, neckLeft],
|
||||
tailWidth,
|
||||
true,
|
||||
minHalfWidth,
|
||||
),
|
||||
neckLeft,
|
||||
]
|
||||
// const leftPoints = DrawUtil.getBezierPoints(leftControls)
|
||||
const leftPoints = DrawUtil.getQBSplinePoints(leftControls)
|
||||
// innerPoints.reverse()
|
||||
const rightControls = [
|
||||
neckRight,
|
||||
...DrawUtil.getArrowAsideControlPoints(
|
||||
[tailRight, ...innerPoints, neckRight],
|
||||
tailWidth,
|
||||
false,
|
||||
minHalfWidth,
|
||||
),
|
||||
tailRight,
|
||||
]
|
||||
// const rightPoints = DrawUtil.getBezierPoints(rightControls)
|
||||
const rightPoints = DrawUtil.getQBSplinePoints(rightControls)
|
||||
const pts = [
|
||||
tailLeft,
|
||||
...leftPoints,
|
||||
neckLeft,
|
||||
headLeft,
|
||||
pntHead,
|
||||
headRight,
|
||||
neckRight,
|
||||
...rightPoints,
|
||||
tailRight,
|
||||
]
|
||||
|
||||
return DrawUtil.positionDistinct(pts.map((p) => Cesium.Cartesian3.fromDegrees(p[0], p[1])))
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import * as Cesium from 'cesium'
|
||||
import DrawUtil from './drawUtil'
|
||||
|
||||
// const tailWidthFactor = 0.07
|
||||
const neckWidthFactor = 0.07
|
||||
const headWidthFactor = 0.1
|
||||
const headAngle = Math.PI - Math.PI / 8.5
|
||||
const neckAngle = Math.PI - Math.PI / 13
|
||||
|
||||
// 创建箭头点数组
|
||||
export function createAttackStraightArrowPositions(worldPositions) {
|
||||
// let pnts = Parse.parsePolygonCoordToArray(
|
||||
// Transform.transformCartesianArrayToWGS84Array(this._positions)
|
||||
// )[0]
|
||||
let pnts = worldPositions.map((car3) => {
|
||||
const carto = Cesium.Cartographic.fromCartesian(car3)
|
||||
return [Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude)]
|
||||
})
|
||||
// 箭头尾部中间
|
||||
let pnt1 = DrawUtil.mid(pnts[0], pnts[1])
|
||||
// 箭头顶点
|
||||
let pnt2 = pnts[2]
|
||||
let len = DrawUtil.getBaseLength(pnts)
|
||||
let tailWidth = DrawUtil.getBaseLength(pnts.slice(0, 2)) / 2
|
||||
let neckWidth = len * neckWidthFactor
|
||||
let headWidth = len * headWidthFactor
|
||||
const isClockWise = DrawUtil.isClockWise(pnts[0], pnts[1], pnts[2])
|
||||
// 箭头左尾点
|
||||
let tailLeft = isClockWise ? pnts[0] : pnts[1]
|
||||
// 箭头右尾点
|
||||
let tailRight = isClockWise ? pnts[1] : pnts[0]
|
||||
// 箭头左端点
|
||||
let headLeft = DrawUtil.getThirdPoint(pnt1, pnt2, headAngle, headWidth, false)
|
||||
// 箭头右端点
|
||||
let headRight = DrawUtil.getThirdPoint(pnt1, pnt2, headAngle, headWidth, true)
|
||||
// 箭头左颈点
|
||||
let neckLeft = DrawUtil.getThirdPoint(pnt1, pnt2, neckAngle, neckWidth, false)
|
||||
// 箭头右颈点
|
||||
let neckRight = DrawUtil.getThirdPoint(pnt1, pnt2, neckAngle, neckWidth, true)
|
||||
const pts = [tailLeft, neckLeft, headLeft, pnt2, headRight, neckRight, tailRight]
|
||||
return DrawUtil.positionDistinct(pts.map((p) => Cesium.Cartesian3.fromDegrees(p[0], p[1])))
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import * as Cesium from 'cesium'
|
||||
import DrawUtil from './drawUtil'
|
||||
|
||||
const FITTING_COUNT = 100
|
||||
const t = 0.4
|
||||
|
||||
export function createCurvePolygonPositions(worldPositions) {
|
||||
let pnts = worldPositions.map((car3) => {
|
||||
const carto = Cesium.Cartographic.fromCartesian(car3)
|
||||
return [Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude)]
|
||||
})
|
||||
|
||||
if (pnts.length === 2) {
|
||||
let mid = DrawUtil.mid(pnts[0], pnts[1])
|
||||
let d = DrawUtil.distance(pnts[0], mid) / 2
|
||||
let pntLeft = DrawUtil.getThirdPoint(pnts[0], mid, Cesium.Math.PI_OVER_TWO, d, false)
|
||||
let pntRight = DrawUtil.getThirdPoint(pnts[0], mid, Cesium.Math.PI_OVER_TWO, d, true)
|
||||
pnts = [pnts[0], pntLeft, pnts[1], pntRight]
|
||||
}
|
||||
// let mid = DrawUtil.mid(pnts[0], pnts[2])
|
||||
pnts.push(pnts[0], pnts[1])
|
||||
let normals = []
|
||||
for (let i = 0; i < pnts.length - 2; i++) {
|
||||
let pnt1 = pnts[i]
|
||||
let pnt2 = pnts[i + 1]
|
||||
let pnt3 = pnts[i + 2]
|
||||
let normalPoints = DrawUtil.getBisectorNormals(t, pnt1, pnt2, pnt3)
|
||||
normals = normals.concat(normalPoints)
|
||||
}
|
||||
let count = normals.length
|
||||
normals = [normals[count - 1]].concat(normals.slice(0, count - 1))
|
||||
let pList = []
|
||||
for (let i = 0; i < pnts.length - 2; i++) {
|
||||
let pnt1 = pnts[i]
|
||||
let pnt2 = pnts[i + 1]
|
||||
pList.push(pnt1)
|
||||
for (let t = 0; t <= FITTING_COUNT; t++) {
|
||||
let pnt = DrawUtil.getCubicValue(
|
||||
t / FITTING_COUNT,
|
||||
pnt1,
|
||||
normals[i * 2],
|
||||
normals[i * 2 + 1],
|
||||
pnt2,
|
||||
)
|
||||
pList.push(pnt)
|
||||
}
|
||||
pList.push(pnt2)
|
||||
}
|
||||
return DrawUtil.positionDistinct(pList.map((p) => Cesium.Cartesian3.fromDegrees(p[0], p[1])))
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
import * as Cesium from 'cesium'
|
||||
import DrawUtil from './drawUtil'
|
||||
|
||||
// const tailWidthFactor = 0.07
|
||||
// const neckWidthFactor = 0.07
|
||||
// const headWidthFactor = 0.10
|
||||
|
||||
const headHeightFactor = 0.25
|
||||
const headWidthFactor = 0.3
|
||||
const neckHeightFactor = 0.85
|
||||
const neckWidthFactor = 0.15
|
||||
const headAngle = Math.PI - Math.PI / 8.5
|
||||
const neckAngle = Math.PI - Math.PI / 13
|
||||
|
||||
export function createDoubleArrowPositions(worldPositions) {
|
||||
let count = worldPositions.length
|
||||
let tempPoint4 = undefined
|
||||
let connPoint = undefined
|
||||
let pnts = worldPositions.map((car3) => {
|
||||
const carto = Cesium.Cartographic.fromCartesian(car3)
|
||||
return [Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude)]
|
||||
})
|
||||
let pnt1 = pnts[0]
|
||||
let pnt2 = pnts[1]
|
||||
let pnt3 = pnts[2]
|
||||
if (count === 3) tempPoint4 = getTempPoint4(pnt1, pnt2, pnt3)
|
||||
else tempPoint4 = pnts[3]
|
||||
if (count === 3 || count === 4) connPoint = DrawUtil.mid(pnt1, pnt2)
|
||||
else connPoint = pnts[4]
|
||||
let leftArrowPnts, rightArrowPnts
|
||||
if (DrawUtil.isClockWise(pnt1, pnt2, pnt3)) {
|
||||
leftArrowPnts = getArrowPoints(pnt1, connPoint, tempPoint4, false)
|
||||
rightArrowPnts = getArrowPoints(connPoint, pnt2, pnt3, true)
|
||||
} else {
|
||||
leftArrowPnts = getArrowPoints(pnt2, connPoint, pnt3, false)
|
||||
rightArrowPnts = getArrowPoints(connPoint, pnt1, tempPoint4, true)
|
||||
}
|
||||
let m = leftArrowPnts.length
|
||||
let t = (m - 5) / 2
|
||||
let llBodyPnts = leftArrowPnts.slice(0, t)
|
||||
let lArrowPnts = leftArrowPnts.slice(t, t + 5)
|
||||
let lrBodyPnts = leftArrowPnts.slice(t + 5, m)
|
||||
let rlBodyPnts = rightArrowPnts.slice(0, t)
|
||||
let rArrowPnts = rightArrowPnts.slice(t, t + 5)
|
||||
let rrBodyPnts = rightArrowPnts.slice(t + 5, m)
|
||||
rlBodyPnts = DrawUtil.getBezierPoints(rlBodyPnts)
|
||||
let bodyPnts = DrawUtil.getBezierPoints(rrBodyPnts.concat(llBodyPnts.slice(1)))
|
||||
lrBodyPnts = DrawUtil.getBezierPoints(lrBodyPnts)
|
||||
// return new Cesium.PolygonHierarchy(
|
||||
// Transform.transformWGS84ArrayToCartesianArray(
|
||||
// Parse.parsePositions(
|
||||
// rlBodyPnts.concat(rArrowPnts, bodyPnts, lArrowPnts, lrBodyPnts)
|
||||
// )
|
||||
// )
|
||||
// )
|
||||
const pts = rlBodyPnts.concat(rArrowPnts, bodyPnts, lArrowPnts, lrBodyPnts)
|
||||
return pts.map((p) => Cesium.Cartesian3.fromDegrees(p[0], p[1]))
|
||||
}
|
||||
|
||||
function getArrowPoints(pnt1, pnt2, pnt3, clockWise) {
|
||||
let midPnt = DrawUtil.mid(pnt1, pnt2)
|
||||
let len = DrawUtil.distance(midPnt, pnt3)
|
||||
let midPnt1 = DrawUtil.getThirdPoint(pnt3, midPnt, Cesium.Math.PI, len * 0.3, true)
|
||||
let midPnt2 = DrawUtil.getThirdPoint(pnt3, midPnt, Cesium.Math.PI, len * 0.5, true)
|
||||
midPnt1 = DrawUtil.getThirdPoint(midPnt, midPnt1, Cesium.Math.PI_OVER_TWO, len / 5, clockWise)
|
||||
midPnt2 = DrawUtil.getThirdPoint(midPnt, midPnt2, Cesium.Math.PI_OVER_TWO, len / 4, clockWise)
|
||||
let points = [midPnt, midPnt1, midPnt2, pnt3]
|
||||
// 计算箭头部分
|
||||
let arrowPnts = getArrowHeadPoints(points)
|
||||
let neckLeftPoint = arrowPnts[0]
|
||||
let neckRightPoint = arrowPnts[4]
|
||||
// 计算箭身部分
|
||||
let tailWidthFactor = DrawUtil.distance(pnt1, pnt2) / DrawUtil.getBaseLength(points) / 2
|
||||
let bodyPnts = getArrowBodyPoints(points, neckLeftPoint, neckRightPoint, tailWidthFactor)
|
||||
let n = bodyPnts.length
|
||||
let lPoints = bodyPnts.slice(0, n / 2)
|
||||
let rPoints = bodyPnts.slice(n / 2, n)
|
||||
lPoints.push(neckLeftPoint)
|
||||
rPoints.push(neckRightPoint)
|
||||
lPoints = lPoints.reverse()
|
||||
lPoints.push(pnt2)
|
||||
rPoints = rPoints.reverse()
|
||||
rPoints.push(pnt1)
|
||||
return lPoints.reverse().concat(arrowPnts, rPoints)
|
||||
}
|
||||
|
||||
function getArrowHeadPoints(points) {
|
||||
let len = DrawUtil.getBaseLength(points)
|
||||
let headHeight = len * headHeightFactor
|
||||
let headPnt = points[points.length - 1]
|
||||
let headWidth = headHeight * headWidthFactor
|
||||
let neckWidth = headHeight * neckWidthFactor
|
||||
let neckHeight = headHeight * neckHeightFactor
|
||||
let headEndPnt = DrawUtil.getThirdPoint(
|
||||
points[points.length - 2],
|
||||
headPnt,
|
||||
Cesium.Math.PI,
|
||||
headHeight,
|
||||
true,
|
||||
)
|
||||
let neckEndPnt = DrawUtil.getThirdPoint(
|
||||
points[points.length - 2],
|
||||
headPnt,
|
||||
Cesium.Math.PI,
|
||||
neckHeight,
|
||||
true,
|
||||
)
|
||||
let headLeft = DrawUtil.getThirdPoint(
|
||||
headPnt,
|
||||
headEndPnt,
|
||||
Cesium.Math.PI_OVER_TWO,
|
||||
headWidth,
|
||||
false,
|
||||
)
|
||||
let headRight = DrawUtil.getThirdPoint(
|
||||
headPnt,
|
||||
headEndPnt,
|
||||
Cesium.Math.PI_OVER_TWO,
|
||||
headWidth,
|
||||
true,
|
||||
)
|
||||
let neckLeft = DrawUtil.getThirdPoint(
|
||||
headPnt,
|
||||
neckEndPnt,
|
||||
Cesium.Math.PI_OVER_TWO,
|
||||
neckWidth,
|
||||
false,
|
||||
)
|
||||
let neckRight = DrawUtil.getThirdPoint(
|
||||
headPnt,
|
||||
neckEndPnt,
|
||||
Cesium.Math.PI_OVER_TWO,
|
||||
neckWidth,
|
||||
true,
|
||||
)
|
||||
|
||||
return [neckLeft, headLeft, headPnt, headRight, neckRight]
|
||||
}
|
||||
|
||||
function getArrowBodyPoints(points, neckLeft, neckRight, tailWidthFactor) {
|
||||
let allLen = DrawUtil.wholeDistance(points)
|
||||
let len = DrawUtil.getBaseLength(points)
|
||||
let tailWidth = len * tailWidthFactor
|
||||
let neckWidth = DrawUtil.distance(neckLeft, neckRight)
|
||||
let widthDif = (tailWidth - neckWidth) / 2
|
||||
let tempLen = 0
|
||||
let leftBodyPnts = []
|
||||
let rightBodyPnts = []
|
||||
for (let i = 1; i < points.length - 1; i++) {
|
||||
let angle = DrawUtil.getAngleOfThreePoints(points[i - 1], points[i], points[i + 1]) / 2
|
||||
tempLen += DrawUtil.distance(points[i - 1], points[i])
|
||||
let w = (tailWidth / 2 - (tempLen / allLen) * widthDif) / Math.sin(angle)
|
||||
let left = DrawUtil.getThirdPoint(points[i - 1], points[i], Math.PI - angle, w, true)
|
||||
let right = DrawUtil.getThirdPoint(points[i - 1], points[i], angle, w, false)
|
||||
leftBodyPnts.push(left)
|
||||
rightBodyPnts.push(right)
|
||||
}
|
||||
return leftBodyPnts.concat(rightBodyPnts)
|
||||
}
|
||||
|
||||
function getTempPoint4(linePnt1, linePnt2, point) {
|
||||
let midPnt = DrawUtil.mid(linePnt1, linePnt2)
|
||||
let len = DrawUtil.distance(midPnt, point)
|
||||
let angle = DrawUtil.getAngleOfThreePoints(linePnt1, midPnt, point)
|
||||
let symPnt, distance1, distance2, mid
|
||||
if (angle < Cesium.Math.PI_OVER_TWO) {
|
||||
distance1 = len * Math.sin(angle)
|
||||
distance2 = len * Math.cos(angle)
|
||||
mid = DrawUtil.getThirdPoint(linePnt1, midPnt, Cesium.Math.PI_OVER_TWO, distance1, false)
|
||||
symPnt = DrawUtil.getThirdPoint(midPnt, mid, Cesium.Math.PI_OVER_TWO, distance2, true)
|
||||
} else if (angle >= Cesium.Math.PI_OVER_TWO && angle < Math.PI) {
|
||||
distance1 = len * Math.sin(Math.PI - angle)
|
||||
distance2 = len * Math.cos(Math.PI - angle)
|
||||
mid = DrawUtil.getThirdPoint(linePnt1, midPnt, Cesium.Math.PI_OVER_TWO, distance1, false)
|
||||
symPnt = DrawUtil.getThirdPoint(midPnt, mid, Cesium.Math.PI_OVER_TWO, distance2, false)
|
||||
} else if (angle >= Math.PI && angle < Math.PI * 1.5) {
|
||||
distance1 = len * Math.sin(angle - Math.PI)
|
||||
distance2 = len * Math.cos(angle - Math.PI)
|
||||
mid = DrawUtil.getThirdPoint(linePnt1, midPnt, Cesium.Math.PI_OVER_TWO, distance1, true)
|
||||
symPnt = DrawUtil.getThirdPoint(midPnt, mid, Cesium.Math.PI_OVER_TWO, distance2, true)
|
||||
} else {
|
||||
distance1 = len * Math.sin(Math.PI * 2 - angle)
|
||||
distance2 = len * Math.cos(Math.PI * 2 - angle)
|
||||
mid = DrawUtil.getThirdPoint(linePnt1, midPnt, Cesium.Math.PI_OVER_TWO, distance1, true)
|
||||
symPnt = DrawUtil.getThirdPoint(midPnt, mid, Cesium.Math.PI_OVER_TWO, distance2, false)
|
||||
}
|
||||
return symPnt
|
||||
}
|
|
@ -0,0 +1,672 @@
|
|||
/**
|
||||
* @Author : Caven Chen
|
||||
*/
|
||||
import * as Cesium from 'cesium'
|
||||
import * as turf from '@turf/turf'
|
||||
|
||||
const TWO_PI = Math.PI * 2
|
||||
const FITTING_COUNT = 100
|
||||
const ZERO_TOLERANCE = 0.0001
|
||||
|
||||
class DrawUtil {
|
||||
/**
|
||||
* @param pnt1
|
||||
* @param pnt2
|
||||
* @returns {number}
|
||||
*/
|
||||
static distance(pnt1, pnt2) {
|
||||
return Math.sqrt(Math.pow(pnt1[0] - pnt2[0], 2) + Math.pow(pnt1[1] - pnt2[1], 2))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param points
|
||||
* @returns {number}
|
||||
*/
|
||||
static wholeDistance(points) {
|
||||
let distance = 0
|
||||
for (let i = 0; i < points.length - 1; i++) distance += this.distance(points[i], points[i + 1])
|
||||
return distance
|
||||
}
|
||||
|
||||
/**
|
||||
* 把经纬度当平面坐标计算长度
|
||||
* @param points
|
||||
* @returns {number}
|
||||
*/
|
||||
// static getBaseLength(points) {
|
||||
// return Math.pow(this.wholeDistance(points), 0.99)
|
||||
// }
|
||||
static getBaseLength(points) {
|
||||
let len = 0
|
||||
if (points.length < 2) return len
|
||||
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
len += Math.sqrt(
|
||||
Math.pow(points[i][0] - points[i - 1][0], 2) + Math.pow(points[i][1] - points[i - 1][1], 2),
|
||||
)
|
||||
}
|
||||
return len
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pnt1
|
||||
* @param pnt2
|
||||
* @returns {number[]}
|
||||
*/
|
||||
static mid(pnt1, pnt2) {
|
||||
return [(pnt1[0] + pnt2[0]) / 2, (pnt1[1] + pnt2[1]) / 2]
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pnt1
|
||||
* @param pnt2
|
||||
* @param pnt3
|
||||
* @returns {[*, *]|[*, *]|[*, number]}
|
||||
*/
|
||||
static getCircleCenterOfThreePoints(pnt1, pnt2, pnt3) {
|
||||
const pntA = [(pnt1[0] + pnt2[0]) / 2, (pnt1[1] + pnt2[1]) / 2]
|
||||
const pntB = [pntA[0] - pnt1[1] + pnt2[1], pntA[1] + pnt1[0] - pnt2[0]]
|
||||
const pntC = [(pnt1[0] + pnt3[0]) / 2, (pnt1[1] + pnt3[1]) / 2]
|
||||
const pntD = [pntC[0] - pnt1[1] + pnt3[1], pntC[1] + pnt1[0] - pnt3[0]]
|
||||
return this.getIntersectPoint(pntA, pntB, pntC, pntD)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pntA
|
||||
* @param pntB
|
||||
* @param pntC
|
||||
* @param pntD
|
||||
* @returns {(*|number)[]|*[]}
|
||||
*/
|
||||
static getIntersectPoint(pntA, pntB, pntC, pntD) {
|
||||
let x, y, f, e
|
||||
if (pntA[1] === pntB[1]) {
|
||||
f = (pntD[0] - pntC[0]) / (pntD[1] - pntC[1])
|
||||
x = f * (pntA[1] - pntC[1]) + pntC[0]
|
||||
y = pntA[1]
|
||||
return [x, y]
|
||||
}
|
||||
if (pntC[1] === pntD[1]) {
|
||||
e = (pntB[0] - pntA[0]) / (pntB[1] - pntA[1])
|
||||
x = e * (pntC[1] - pntA[1]) + pntA[0]
|
||||
y = pntC[1]
|
||||
return [x, y]
|
||||
}
|
||||
e = (pntB[0] - pntA[0]) / (pntB[1] - pntA[1])
|
||||
f = (pntD[0] - pntC[0]) / (pntD[1] - pntC[1])
|
||||
y = (e * pntA[1] - pntA[0] - f * pntC[1] + pntC[0]) / (e - f)
|
||||
x = e * y - e * pntA[1] + pntA[0]
|
||||
return [x, y]
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算方位角
|
||||
* @param startPnt
|
||||
* @param endPnt
|
||||
* @returns {number}
|
||||
*/
|
||||
static getAzimuth(startPnt, endPnt) {
|
||||
const lon1 = startPnt[0]
|
||||
const lat1 = startPnt[1]
|
||||
const lon2 = endPnt[0]
|
||||
const lat2 = endPnt[1]
|
||||
|
||||
if (lon1 === lon2) {
|
||||
if (lat2 >= lat1) {
|
||||
return 0
|
||||
} else {
|
||||
return Math.PI
|
||||
}
|
||||
}
|
||||
if (lat1 === lat2) {
|
||||
if (lon2 > lon1) {
|
||||
return Cesium.Math.PI_OVER_TWO
|
||||
} else if (lon2 < lon1) {
|
||||
return Cesium.Math.THREE_PI_OVER_TWO
|
||||
}
|
||||
}
|
||||
|
||||
const a = Math.atan(((lon2 - lon1) * Math.cos(lat2)) / (lat2 - lat1))
|
||||
if (lat2 > lat1) {
|
||||
if (lon2 > lon1) {
|
||||
return a
|
||||
} else {
|
||||
return a + Cesium.Math.TWO_PI
|
||||
}
|
||||
} else {
|
||||
return a + Cesium.Math.PI
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pntA
|
||||
* @param pntB
|
||||
* @param pntC
|
||||
* @returns {number}
|
||||
*/
|
||||
static getAngleOfThreePoints(pntA, pntB, pntC) {
|
||||
const angle = this.getAzimuth(pntB, pntC) - this.getAzimuth(pntB, pntA)
|
||||
return angle < 0 ? angle + TWO_PI : angle
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pnt1
|
||||
* @param pnt2
|
||||
* @param pnt3
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static isClockWise(pnt1, pnt2, pnt3) {
|
||||
return (pnt3[1] - pnt1[1]) * (pnt2[0] - pnt1[0]) > (pnt2[1] - pnt1[1]) * (pnt3[0] - pnt1[0])
|
||||
// return (pnt2[1] - pnt1[1]) * (pnt3[0] - pnt1[0]) > (pnt2[1] - pnt1[0]) * (pnt3[1] - pnt1[1])
|
||||
}
|
||||
|
||||
/**
|
||||
* @param t
|
||||
* @param startPnt
|
||||
* @param endPnt
|
||||
* @returns {*[]}
|
||||
*/
|
||||
static getPointOnLine(t, startPnt, endPnt) {
|
||||
const x = startPnt[0] + t * (endPnt[0] - startPnt[0])
|
||||
const y = startPnt[1] + t * (endPnt[1] - startPnt[1])
|
||||
return [x, y]
|
||||
}
|
||||
|
||||
/**
|
||||
* @param t
|
||||
* @param startPnt
|
||||
* @param cPnt1
|
||||
* @param cPnt2
|
||||
* @param endPnt
|
||||
* @returns {number[]}
|
||||
*/
|
||||
static getCubicValue(t, startPnt, cPnt1, cPnt2, endPnt) {
|
||||
t = Math.max(Math.min(t, 1), 0)
|
||||
const tp = 1 - t
|
||||
const t2 = t * t
|
||||
const t3 = t2 * t
|
||||
const tp2 = tp * tp
|
||||
const tp3 = tp2 * tp
|
||||
const x = tp3 * startPnt[0] + 3 * tp2 * t * cPnt1[0] + 3 * tp * t2 * cPnt2[0] + t3 * endPnt[0]
|
||||
const y = tp3 * startPnt[1] + 3 * tp2 * t * cPnt1[1] + 3 * tp * t2 * cPnt2[1] + t3 * endPnt[1]
|
||||
return [x, y]
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据第一点和第二点,从第二点沿固定角度延长一定距离,得到第三点
|
||||
* @param startPnt
|
||||
* @param endPnt
|
||||
* @param angle
|
||||
* @param distance
|
||||
* @param clockWise
|
||||
* @returns {*[]}
|
||||
*/
|
||||
static getThirdPoint(startPnt, endPnt, angle, distance, clockWise) {
|
||||
const azimuth = this.getAzimuth(startPnt, endPnt)
|
||||
const alpha = clockWise ? azimuth + angle : azimuth - angle
|
||||
const dx = distance * Math.sin(alpha)
|
||||
const dy = distance * Math.cos(alpha)
|
||||
return [endPnt[0] + dx, endPnt[1] + dy]
|
||||
}
|
||||
|
||||
/**
|
||||
* @param center
|
||||
* @param radius
|
||||
* @param startAngle
|
||||
* @param endAngle
|
||||
* @returns {[]}
|
||||
*/
|
||||
static getArcPoints(center, radius, startAngle, endAngle) {
|
||||
let x,
|
||||
y,
|
||||
pnts = []
|
||||
let angleDiff = endAngle - startAngle
|
||||
angleDiff = angleDiff < 0 ? angleDiff + TWO_PI : angleDiff
|
||||
for (let i = 0; i <= FITTING_COUNT; i++) {
|
||||
const angle = startAngle + (angleDiff * i) / FITTING_COUNT
|
||||
x = center[0] + radius * Math.cos(angle)
|
||||
y = center[1] + radius * Math.sin(angle)
|
||||
pnts.push([x, y])
|
||||
}
|
||||
return pnts
|
||||
}
|
||||
|
||||
/**
|
||||
* @param t
|
||||
* @param pnt1
|
||||
* @param pnt2
|
||||
* @param pnt3
|
||||
* @returns {*[][]}
|
||||
*/
|
||||
static getBisectorNormals(t, pnt1, pnt2, pnt3) {
|
||||
const normal = this.getNormal(pnt1, pnt2, pnt3)
|
||||
const dist = Math.sqrt(normal[0] * normal[0] + normal[1] * normal[1])
|
||||
const uX = normal[0] / dist
|
||||
const uY = normal[1] / dist
|
||||
const d1 = this.distance(pnt1, pnt2)
|
||||
const d2 = this.distance(pnt2, pnt3)
|
||||
let dt, x, y, bisectorNormalLeft, bisectorNormalRight
|
||||
if (dist > ZERO_TOLERANCE) {
|
||||
if (this.isClockWise(pnt1, pnt2, pnt3)) {
|
||||
dt = t * d1
|
||||
x = pnt2[0] - dt * uY
|
||||
y = pnt2[1] + dt * uX
|
||||
bisectorNormalRight = [x, y]
|
||||
dt = t * d2
|
||||
x = pnt2[0] + dt * uY
|
||||
y = pnt2[1] - dt * uX
|
||||
bisectorNormalLeft = [x, y]
|
||||
} else {
|
||||
dt = t * d1
|
||||
x = pnt2[0] + dt * uY
|
||||
y = pnt2[1] - dt * uX
|
||||
bisectorNormalRight = [x, y]
|
||||
dt = t * d2
|
||||
x = pnt2[0] - dt * uY
|
||||
y = pnt2[1] + dt * uX
|
||||
bisectorNormalLeft = [x, y]
|
||||
}
|
||||
} else {
|
||||
x = pnt2[0] + t * (pnt1[0] - pnt2[0])
|
||||
y = pnt2[1] + t * (pnt1[1] - pnt2[1])
|
||||
bisectorNormalRight = [x, y]
|
||||
x = pnt2[0] + t * (pnt3[0] - pnt2[0])
|
||||
y = pnt2[1] + t * (pnt3[1] - pnt2[1])
|
||||
bisectorNormalLeft = [x, y]
|
||||
}
|
||||
return [bisectorNormalRight, bisectorNormalLeft]
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pnt1
|
||||
* @param pnt2
|
||||
* @param pnt3
|
||||
* @returns {number[]}
|
||||
*/
|
||||
static getNormal(pnt1, pnt2, pnt3) {
|
||||
let dX1 = pnt1[0] - pnt2[0]
|
||||
let dY1 = pnt1[1] - pnt2[1]
|
||||
const d1 = Math.sqrt(dX1 * dX1 + dY1 * dY1)
|
||||
dX1 /= d1
|
||||
dY1 /= d1
|
||||
|
||||
let dX2 = pnt3[0] - pnt2[0]
|
||||
let dY2 = pnt3[1] - pnt2[1]
|
||||
const d2 = Math.sqrt(dX2 * dX2 + dY2 * dY2)
|
||||
dX2 /= d2
|
||||
dY2 /= d2
|
||||
|
||||
const uX = dX1 + dX2
|
||||
const uY = dY1 + dY2
|
||||
return [uX, uY]
|
||||
}
|
||||
|
||||
/**
|
||||
* @param t
|
||||
* @param controlPoints
|
||||
* @returns {[]}
|
||||
*/
|
||||
static getCurvePoints(t, controlPoints) {
|
||||
const leftControl = this.getLeftMostControlPoint(t, controlPoints)
|
||||
let normals = [leftControl]
|
||||
let pnt1, pnt2, pnt3, normalPoints
|
||||
for (let i = 0; i < controlPoints.length - 2; i++) {
|
||||
pnt1 = controlPoints[i]
|
||||
pnt2 = controlPoints[i + 1]
|
||||
pnt3 = controlPoints[i + 2]
|
||||
normalPoints = this.getBisectorNormals(t, pnt1, pnt2, pnt3)
|
||||
normals = normals.concat(normalPoints)
|
||||
}
|
||||
const rightControl = this.getRightMostControlPoint(t, controlPoints)
|
||||
normals.push(rightControl)
|
||||
const points = []
|
||||
for (let i = 0; i < controlPoints.length - 1; i++) {
|
||||
pnt1 = controlPoints[i]
|
||||
pnt2 = controlPoints[i + 1]
|
||||
points.push(pnt1)
|
||||
for (let t = 0; t < FITTING_COUNT; t++) {
|
||||
const pnt = this.getCubicValue(
|
||||
t / FITTING_COUNT,
|
||||
pnt1,
|
||||
normals[i * 2],
|
||||
normals[i * 2 + 1],
|
||||
pnt2,
|
||||
)
|
||||
points.push(pnt)
|
||||
}
|
||||
points.push(pnt2)
|
||||
}
|
||||
return points
|
||||
}
|
||||
|
||||
/**
|
||||
* @param t
|
||||
* @param controlPoints
|
||||
* @returns {number[]}
|
||||
*/
|
||||
static getLeftMostControlPoint(t, controlPoints) {
|
||||
const pnt1 = controlPoints[0]
|
||||
const pnt2 = controlPoints[1]
|
||||
const pnt3 = controlPoints[2]
|
||||
const pnts = this.getBisectorNormals(0, pnt1, pnt2, pnt3)
|
||||
const normalRight = pnts[0]
|
||||
const normal = this.getNormal(pnt1, pnt2, pnt3)
|
||||
const dist = Math.sqrt(normal[0] * normal[0] + normal[1] * normal[1])
|
||||
let controlX, controlY
|
||||
if (dist > ZERO_TOLERANCE) {
|
||||
const mid = this.mid(pnt1, pnt2)
|
||||
const pX = pnt1[0] - mid[0]
|
||||
const pY = pnt1[1] - mid[1]
|
||||
const d1 = this.distance(pnt1, pnt2)
|
||||
// normal at midpoint
|
||||
const n = 2.0 / d1
|
||||
const nX = -n * pY
|
||||
const nY = n * pX
|
||||
// upper triangle of symmetric transform matrix
|
||||
const a11 = nX * nX - nY * nY
|
||||
const a12 = 2 * nX * nY
|
||||
const a22 = nY * nY - nX * nX
|
||||
const dX = normalRight[0] - mid[0]
|
||||
const dY = normalRight[1] - mid[1]
|
||||
// coordinates of reflected vector
|
||||
controlX = mid[0] + a11 * dX + a12 * dY
|
||||
controlY = mid[1] + a12 * dX + a22 * dY
|
||||
} else {
|
||||
controlX = pnt1[0] + t * (pnt2[0] - pnt1[0])
|
||||
controlY = pnt1[1] + t * (pnt2[1] - pnt1[1])
|
||||
}
|
||||
return [controlX, controlY]
|
||||
}
|
||||
|
||||
/**
|
||||
* @param t
|
||||
* @param controlPoints
|
||||
* @returns {number[]}
|
||||
*/
|
||||
static getRightMostControlPoint(t, controlPoints) {
|
||||
const count = controlPoints.length
|
||||
const pnt1 = controlPoints[count - 3]
|
||||
const pnt2 = controlPoints[count - 2]
|
||||
const pnt3 = controlPoints[count - 1]
|
||||
const pnts = this.getBisectorNormals(0, pnt1, pnt2, pnt3)
|
||||
const normalLeft = pnts[1]
|
||||
const normal = this.getNormal(pnt1, pnt2, pnt3)
|
||||
const dist = Math.sqrt(normal[0] * normal[0] + normal[1] * normal[1])
|
||||
let controlX, controlY
|
||||
if (dist > ZERO_TOLERANCE) {
|
||||
const mid = this.mid(pnt2, pnt3)
|
||||
const pX = pnt3[0] - mid[0]
|
||||
const pY = pnt3[1] - mid[1]
|
||||
|
||||
const d1 = this.distance(pnt2, pnt3)
|
||||
// normal at midpoint
|
||||
const n = 2.0 / d1
|
||||
const nX = -n * pY
|
||||
const nY = n * pX
|
||||
|
||||
// upper triangle of symmetric transform matrix
|
||||
const a11 = nX * nX - nY * nY
|
||||
const a12 = 2 * nX * nY
|
||||
const a22 = nY * nY - nX * nX
|
||||
|
||||
const dX = normalLeft[0] - mid[0]
|
||||
const dY = normalLeft[1] - mid[1]
|
||||
|
||||
// coordinates of reflected vector
|
||||
controlX = mid[0] + a11 * dX + a12 * dY
|
||||
controlY = mid[1] + a12 * dX + a22 * dY
|
||||
} else {
|
||||
controlX = pnt3[0] + t * (pnt2[0] - pnt3[0])
|
||||
controlY = pnt3[1] + t * (pnt2[1] - pnt3[1])
|
||||
}
|
||||
return [controlX, controlY]
|
||||
}
|
||||
|
||||
/**
|
||||
* @param points
|
||||
* @returns {[]|*}
|
||||
*/
|
||||
static getBezierPoints(points) {
|
||||
if (points.length <= 2) return points
|
||||
const bezierPoints = []
|
||||
const n = points.length - 1
|
||||
for (let t = 0; t <= 1; t += 0.01) {
|
||||
let x = 0
|
||||
let y = 0
|
||||
for (let index = 0; index <= n; index++) {
|
||||
const factor = this.getBinomialFactor(n, index)
|
||||
const a = Math.pow(t, index)
|
||||
const b = Math.pow(1 - t, n - index)
|
||||
x += factor * a * b * points[index][0]
|
||||
y += factor * a * b * points[index][1]
|
||||
}
|
||||
bezierPoints.push([x, y])
|
||||
}
|
||||
bezierPoints.push(points[n])
|
||||
return bezierPoints
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param n
|
||||
* @param index
|
||||
* @returns {number}
|
||||
*/
|
||||
static getBinomialFactor(n, index) {
|
||||
return this.getFactorial(n) / (this.getFactorial(index) * this.getFactorial(n - index))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param n
|
||||
* @returns {number}
|
||||
*/
|
||||
static getFactorial(n) {
|
||||
if (n <= 1) return 1
|
||||
if (n === 2) return 2
|
||||
if (n === 3) return 6
|
||||
if (n === 4) return 24
|
||||
if (n === 5) return 120
|
||||
let result = 1
|
||||
for (let i = 1; i <= n; i++) result *= i
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @param points
|
||||
* @returns {[]|*}
|
||||
*/
|
||||
static getQBSplinePoints(points) {
|
||||
if (points.length <= 2) return points
|
||||
const n = 2
|
||||
const bSplinePoints = []
|
||||
const m = points.length - n - 1
|
||||
bSplinePoints.push(points[0])
|
||||
for (let i = 0; i <= m; i++) {
|
||||
for (let t = 0; t <= 1; t += 0.05) {
|
||||
let x = 0
|
||||
let y = 0
|
||||
for (let k = 0; k <= n; k++) {
|
||||
const factor = this.getQuadricBSplineFactor(k, t)
|
||||
x += factor * points[i + k][0]
|
||||
y += factor * points[i + k][1]
|
||||
}
|
||||
bSplinePoints.push([x, y])
|
||||
}
|
||||
}
|
||||
bSplinePoints.push(points[points.length - 1])
|
||||
return bSplinePoints
|
||||
}
|
||||
|
||||
/**
|
||||
* @param k
|
||||
* @param t
|
||||
* @returns {number}
|
||||
*/
|
||||
static getQuadricBSplineFactor(k, t) {
|
||||
if (k === 0) return Math.pow(t - 1, 2) / 2
|
||||
if (k === 1) return (-2 * Math.pow(t, 2) + 2 * t + 1) / 2
|
||||
if (k === 2) return Math.pow(t, 2) / 2
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取箭头上单边的曲线的控制点,要求最少三个点
|
||||
* @param points
|
||||
* @param distance
|
||||
* @param isLeft
|
||||
* @returns {[]}
|
||||
*/
|
||||
static getArrowAsideControlPoints(points, distance, isLeft, minHalfWidth = 0) {
|
||||
const pnts = []
|
||||
const len = this.getBaseLength(points)
|
||||
let subLen = 0
|
||||
const isReverseAngle = false
|
||||
for (let i = 1; i < points.length - 1; i++) {
|
||||
const pnt0 = points[i - 1]
|
||||
const pnt1 = points[i]
|
||||
const pnt2 = points[i + 1]
|
||||
|
||||
const line1 = new Cesium.Cartesian2(pnt1[0] - pnt0[0], pnt1[1] - pnt0[1])
|
||||
const line2 = new Cesium.Cartesian2(pnt2[0] - pnt1[0], pnt2[1] - pnt1[1])
|
||||
const isReverseAngle = Cesium.Cartesian2.cross(line1, line2) >= 0
|
||||
const halfAngle =
|
||||
(Cesium.Cartesian2.angleBetween(line1, line2) * (isReverseAngle ? -1 : 1)) / 2
|
||||
// const plusAngle = Cesium.Math.PI_OVER_TWO * ((isLeft ^ isReverseAngle) ? -1 : 1)
|
||||
const plusAngle = Cesium.Math.PI_OVER_TWO * (isLeft ? -1 : 1)
|
||||
// 计算距离
|
||||
subLen += this.distance(pnt0, pnt1)
|
||||
const asideLen = (distance - minHalfWidth) * (1 - subLen / len) + minHalfWidth
|
||||
pnts.push(this.getThirdPoint(pnt0, pnt1, halfAngle + plusAngle, asideLen, true))
|
||||
}
|
||||
if (!isLeft) {
|
||||
pnts.reverse()
|
||||
}
|
||||
return pnts
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取条带上单边的控制点,要求最少两个点
|
||||
* @param {*} points 主干点列表
|
||||
* @param {*} isLeft 是否生成左侧控制点
|
||||
* @param {*} width 单边宽度(米)
|
||||
* @returns 单边控制点列表
|
||||
*/
|
||||
static getStripAsideControlPoints(points, isLeft, width) {
|
||||
const pnts = []
|
||||
if (points.length === 2) {
|
||||
const azimuth = Cesium.Math.toDegrees(this.getAzimuth(points[0], points[1]))
|
||||
const angle = this.getTurfAngle(azimuth + (isLeft ? -90 : 90))
|
||||
pnts.push(this.getTurfDestination(points[0], width, angle))
|
||||
pnts.push(this.getTurfDestination(points[1], width, angle))
|
||||
} else if (points.length > 2) {
|
||||
// 添加第一个点
|
||||
const azimuthFirst = Cesium.Math.toDegrees(this.getAzimuth(points[0], points[1]))
|
||||
const angleFirst = this.getTurfAngle(azimuthFirst + (isLeft ? -90 : 90))
|
||||
pnts.push(this.getTurfDestination(points[0], width, angleFirst))
|
||||
// 添加中间控制点
|
||||
for (let i = 1; i < points.length - 1; i++) {
|
||||
const pnt0 = points[i - 1]
|
||||
const pnt1 = points[i]
|
||||
const pnt2 = points[i + 1]
|
||||
|
||||
// 重写计算二分距离的算法
|
||||
const azimuth0 = Cesium.Math.toDegrees(this.getAzimuth(pnt0, pnt1))
|
||||
const azimuth1 = Cesium.Math.toDegrees(this.getAzimuth(pnt1, pnt2))
|
||||
let angle = this.getTurfAngle((azimuth0 + azimuth1) / 2)
|
||||
const angleDiff = Math.abs(azimuth1 - azimuth0)
|
||||
const isReverseAngle = angleDiff > 180
|
||||
// console.log('azimuth0', azimuth0, 'azimuth1', azimuth1, 'angle', angle, 'azimuth1-azimuth0', azimuth1-azimuth0)
|
||||
angle = this.getTurfAngle(angle + 90 * (isLeft ^ isReverseAngle ? -1 : 1))
|
||||
// console.log('计算后angle', angle)
|
||||
|
||||
// 计算等宽的条带控制点
|
||||
// const azimuth1 = Cesium.Math.toDegrees(this.getAzimuth(pnt0, pnt1))
|
||||
// const angle1 = this.getTurfAngle(azimuth1 + (isLeft ? -90 : 90))
|
||||
// const p11 = this.getTurfDestination(pnt0, width, angle1)
|
||||
// const p12 = this.getTurfDestination(pnt1, width, angle1)
|
||||
// const azimuth2 = Cesium.Math.toDegrees(this.getAzimuth(pnt1, pnt2))
|
||||
// const angle2 = this.getTurfAngle(azimuth2 + (isLeft ? -90 : 90))
|
||||
// const p21 = this.getTurfDestination(pnt1, width, angle2)
|
||||
// const p22 = this.getTurfDestination(pnt2, width, angle2)
|
||||
// if(turf.booleanIntersects(turf.lineString([p11, p12]), turf.lineString([p21, p22]))) {
|
||||
// const interPnt = this.getIntersectPoint(p11, p12, p21, p22)
|
||||
// console.log('interPnt', interPnt)
|
||||
// pnts.push(interPnt)
|
||||
// } else {
|
||||
// pnts.push(p12)
|
||||
// pnts.push(p21)
|
||||
// }
|
||||
|
||||
// 计算距离
|
||||
pnts.push(this.getTurfDestination(pnt1, width, angle))
|
||||
}
|
||||
// 添加最后一个点
|
||||
const azimuthLast = Cesium.Math.toDegrees(
|
||||
this.getAzimuth(points[points.length - 2], points[points.length - 1]),
|
||||
)
|
||||
const angleLast = this.getTurfAngle(azimuthLast + (isLeft ? -90 : 90))
|
||||
pnts.push(this.getTurfDestination(points[points.length - 1], width, angleLast))
|
||||
}
|
||||
// 右侧反序
|
||||
if (!isLeft) {
|
||||
pnts.reverse()
|
||||
}
|
||||
return pnts
|
||||
}
|
||||
|
||||
/**
|
||||
* 坐标点去重
|
||||
* @param {*} worldPositions 坐标点列表
|
||||
*/
|
||||
static positionDistinct(worldPositions) {
|
||||
// 坐标点列表浅拷贝,因为后续的splice作用于数组本身,需要避免影响到传入的参数
|
||||
const positions = [...worldPositions]
|
||||
const pnts = positions.map((car3) => {
|
||||
const carto = Cesium.Cartographic.fromCartesian(car3)
|
||||
return [Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude)]
|
||||
})
|
||||
for (let index = pnts.length - 1; index > 0; index--) {
|
||||
const p1 = pnts[index]
|
||||
const p0 = pnts[index - 1]
|
||||
// 两点之间的距离小于1e-9,认为是同一个点
|
||||
// 删除后一个点
|
||||
if (
|
||||
(p1[0] === p0[0] && p1[1] === p0[1]) ||
|
||||
Math.sqrt(Math.pow(p1[0] - p0[0], 2) + Math.pow(p1[1] - p0[1], 2)) < 1e-9
|
||||
) {
|
||||
positions.splice(index, 1)
|
||||
}
|
||||
}
|
||||
return positions
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取turf中的角度(-180, 180)
|
||||
* @param angle 角度值
|
||||
* @returns
|
||||
*/
|
||||
static getTurfAngle(angle) {
|
||||
let res = angle
|
||||
while (res > 180) {
|
||||
res -= 360
|
||||
}
|
||||
while (res < -180) {
|
||||
res += 360
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取turf中的点
|
||||
* @param pnt 点
|
||||
* @param distance 距离(米)
|
||||
* @param angle 角度
|
||||
* @returns [经度, 纬度]
|
||||
*/
|
||||
static getTurfDestination(pnt, distance, angle) {
|
||||
const json = turf.destination(pnt, distance / 1000, angle)
|
||||
return json.geometry.coordinates
|
||||
}
|
||||
}
|
||||
|
||||
export default DrawUtil
|
|
@ -0,0 +1,41 @@
|
|||
import * as Cesium from 'cesium'
|
||||
import DrawUtil from './drawUtil'
|
||||
|
||||
const tailWidthFactor = 0.07
|
||||
const neckWidthFactor = 0.07
|
||||
const headWidthFactor = 0.1
|
||||
const headAngle = Math.PI - Math.PI / 8.5
|
||||
const neckAngle = Math.PI - Math.PI / 13
|
||||
|
||||
// 创建箭头点数组
|
||||
export function createStraightArrowPositions(worldPositions) {
|
||||
// let pnts = Parse.parsePolygonCoordToArray(
|
||||
// Transform.transformCartesianArrayToWGS84Array(this._positions)
|
||||
// )[0]
|
||||
let pnts = worldPositions.map((car3) => {
|
||||
const carto = Cesium.Cartographic.fromCartesian(car3)
|
||||
return [Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude)]
|
||||
})
|
||||
// 箭头尾部中间
|
||||
let pnt1 = pnts[0]
|
||||
// 箭头顶点
|
||||
let pnt2 = pnts[1]
|
||||
let len = DrawUtil.getBaseLength(pnts)
|
||||
let tailWidth = len * tailWidthFactor
|
||||
let neckWidth = len * neckWidthFactor
|
||||
let headWidth = len * headWidthFactor
|
||||
// 箭头左尾点
|
||||
let tailLeft = DrawUtil.getThirdPoint(pnt2, pnt1, Cesium.Math.PI_OVER_TWO, tailWidth, true)
|
||||
// 箭头右尾点
|
||||
let tailRight = DrawUtil.getThirdPoint(pnt2, pnt1, Cesium.Math.PI_OVER_TWO, tailWidth, false)
|
||||
// 箭头左端点
|
||||
let headLeft = DrawUtil.getThirdPoint(pnt1, pnt2, headAngle, headWidth, false)
|
||||
// 箭头右端点
|
||||
let headRight = DrawUtil.getThirdPoint(pnt1, pnt2, headAngle, headWidth, true)
|
||||
// 箭头左颈点
|
||||
let neckLeft = DrawUtil.getThirdPoint(pnt1, pnt2, neckAngle, neckWidth, false)
|
||||
// 箭头右颈点
|
||||
let neckRight = DrawUtil.getThirdPoint(pnt1, pnt2, neckAngle, neckWidth, true)
|
||||
const pts = [tailLeft, neckLeft, headLeft, pnt2, headRight, neckRight, tailRight]
|
||||
return DrawUtil.positionDistinct(pts.map((p) => Cesium.Cartesian3.fromDegrees(p[0], p[1])))
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
import * as Cesium from 'cesium'
|
||||
import DrawUtil from './drawUtil'
|
||||
import { createStraightArrowPositions } from './straightArrowGraphic'
|
||||
|
||||
const tailWidthFactor = 0.07
|
||||
const neckWidthFactor = 0.07
|
||||
const headWidthFactor = 0.1
|
||||
const headAngle = Math.PI - Math.PI / 8.5
|
||||
const neckAngle = Math.PI - Math.PI / 13
|
||||
|
||||
// 创建箭头点数组
|
||||
export function createWideArrowPositions(worldPositions) {
|
||||
if (!worldPositions || worldPositions.length < 2) {
|
||||
return []
|
||||
} else if (worldPositions.length === 2) {
|
||||
// 2点时,直接创建一个直箭头
|
||||
return createStraightArrowPositions(worldPositions)
|
||||
}
|
||||
|
||||
let pnts = worldPositions.map((car3) => {
|
||||
const carto = Cesium.Cartographic.fromCartesian(car3)
|
||||
return [Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude)]
|
||||
})
|
||||
// 箭头尾部中间
|
||||
let pntTail = pnts[0]
|
||||
// 箭头顶点
|
||||
let pntHead = pnts[pnts.length - 1]
|
||||
// 靠近尾点的点
|
||||
let pntTailNear = pnts[1]
|
||||
// 靠近顶点的点
|
||||
let pntNeck = pnts[pnts.length - 2]
|
||||
|
||||
let len = DrawUtil.getBaseLength(pnts)
|
||||
let tailWidth = len * tailWidthFactor
|
||||
let neckWidth = len * neckWidthFactor
|
||||
let headWidth = len * headWidthFactor
|
||||
// 箭头左尾点
|
||||
let tailLeft = DrawUtil.getThirdPoint(
|
||||
pntTailNear,
|
||||
pntTail,
|
||||
Cesium.Math.PI_OVER_TWO,
|
||||
tailWidth,
|
||||
true,
|
||||
)
|
||||
// 箭头右尾点
|
||||
let tailRight = DrawUtil.getThirdPoint(
|
||||
pntTailNear,
|
||||
pntTail,
|
||||
Cesium.Math.PI_OVER_TWO,
|
||||
tailWidth,
|
||||
false,
|
||||
)
|
||||
// 箭头左端点
|
||||
let headLeft = DrawUtil.getThirdPoint(pntNeck, pntHead, headAngle, headWidth, false)
|
||||
// 箭头右端点
|
||||
let headRight = DrawUtil.getThirdPoint(pntNeck, pntHead, headAngle, headWidth, true)
|
||||
// 箭头左颈点
|
||||
let neckLeft = DrawUtil.getThirdPoint(pntNeck, pntHead, neckAngle, neckWidth, false)
|
||||
// 箭头右颈点
|
||||
let neckRight = DrawUtil.getThirdPoint(pntNeck, pntHead, neckAngle, neckWidth, true)
|
||||
|
||||
// 生成样条曲线
|
||||
// const innerPoints = pnts.slice(1, pnts.length - 1)
|
||||
const minHalfWidth = neckWidth * Math.sin(Math.PI / 13)
|
||||
const leftControls = [
|
||||
tailLeft,
|
||||
...DrawUtil.getArrowAsideControlPoints(pnts, tailWidth, true, minHalfWidth),
|
||||
neckLeft,
|
||||
]
|
||||
// const leftPoints = DrawUtil.getBezierPoints(leftControls)
|
||||
const leftPoints = DrawUtil.getQBSplinePoints(leftControls)
|
||||
// innerPoints.reverse()
|
||||
const rightControls = [
|
||||
neckRight,
|
||||
...DrawUtil.getArrowAsideControlPoints(pnts, tailWidth, false, minHalfWidth),
|
||||
tailRight,
|
||||
]
|
||||
// const rightPoints = DrawUtil.getBezierPoints(rightControls)
|
||||
const rightPoints = DrawUtil.getQBSplinePoints(rightControls)
|
||||
const pts = [
|
||||
tailLeft,
|
||||
...leftPoints,
|
||||
neckLeft,
|
||||
headLeft,
|
||||
pntHead,
|
||||
headRight,
|
||||
neckRight,
|
||||
...rightPoints,
|
||||
tailRight,
|
||||
]
|
||||
|
||||
return DrawUtil.positionDistinct(pts.map((p) => Cesium.Cartesian3.fromDegrees(p[0], p[1])))
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
import {
|
||||
Cartesian3,
|
||||
Entity,
|
||||
CallbackProperty,
|
||||
Color as CesiumColor,
|
||||
ScreenSpaceEventType,
|
||||
PolygonHierarchy,
|
||||
HeightReference,
|
||||
} from 'cesium'
|
||||
import { createAttackArrowPositions } from './draw/attackArrowGraphic'
|
||||
|
||||
/**
|
||||
* 绘制攻击箭头的具体逻辑
|
||||
* @param deps 绘制所需的依赖
|
||||
* @returns Promise 绘制完成的所有点坐标数组
|
||||
*/
|
||||
export const drawAttackArrow = (deps) => {
|
||||
const { viewer, bus, position, status, registerAbort, globalAbort } = deps
|
||||
|
||||
// 如果在执行其它操作,则取消
|
||||
globalAbort()
|
||||
|
||||
status.value = 'Drawing'
|
||||
const featurePositions = []
|
||||
|
||||
// 临时多边形实体 (包含填充和边界线)
|
||||
const tempPolygon = new Entity({
|
||||
name: '__draw_temp_attack_arrow',
|
||||
polygon: {
|
||||
hierarchy: new CallbackProperty(() => {
|
||||
const positions = [...featurePositions]
|
||||
// 添加当前鼠标位置作为最后一个点,形成动态效果
|
||||
if (
|
||||
position.value &&
|
||||
!position.value.equals(featurePositions[featurePositions.length - 1]) // 避免重复添加同一个点
|
||||
) {
|
||||
positions.push(position.value)
|
||||
}
|
||||
// 攻击箭头需要至少3个点形成
|
||||
if (positions.length < 3) {
|
||||
return null
|
||||
}
|
||||
return new PolygonHierarchy(createAttackArrowPositions(positions))
|
||||
}, false),
|
||||
heightReference: HeightReference.CLAMP_TO_GROUND,
|
||||
material: CesiumColor.fromCssColorString('#ff0000').withAlpha(0.3),
|
||||
},
|
||||
// 绘制边界线,连接所有点和第一个点,形成闭合效果
|
||||
polyline: {
|
||||
positions: new CallbackProperty(() => {
|
||||
const positions = [...featurePositions]
|
||||
// 添加当前鼠标位置作为最后一个点
|
||||
if (
|
||||
position.value &&
|
||||
!position.value.equals(featurePositions[featurePositions.length - 1])
|
||||
) {
|
||||
positions.push(position.value)
|
||||
}
|
||||
// 攻击箭头需要至少2个点才能显示线
|
||||
if (positions.length < 2) {
|
||||
return null
|
||||
}
|
||||
// 只有2个点时,显示箭头尾部的线
|
||||
if (positions.length === 2) {
|
||||
return positions
|
||||
}
|
||||
const pts = createAttackArrowPositions(positions)
|
||||
return [...pts, pts[0]]
|
||||
}, false),
|
||||
// clampToGround: true, // 多边形边界线通常不需要 clampToGround,因为多边形本身已经处理了高度
|
||||
width: 2,
|
||||
material: CesiumColor.fromCssColorString('#ff0000'),
|
||||
},
|
||||
})
|
||||
|
||||
viewer.entities.add(tempPolygon)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// 清理资源
|
||||
const dispose = () => {
|
||||
registerAbort(null) // 取消注册当前的 abort 回调
|
||||
status.value = 'Default'
|
||||
viewer.entities.remove(tempPolygon)
|
||||
bus.offScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint)
|
||||
bus.offScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw)
|
||||
bus.offScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw)
|
||||
}
|
||||
|
||||
// 取消绘制
|
||||
const abort = (info) => {
|
||||
dispose()
|
||||
reject(info || '取消攻击箭头绘制!')
|
||||
}
|
||||
|
||||
// 左键加点
|
||||
const handleAddPoint = () => {
|
||||
if (position.value && !position.value.equals(featurePositions[featurePositions.length - 1])) {
|
||||
featurePositions.push(position.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 左键双击完成绘制
|
||||
const handleEndDraw = () => {
|
||||
// 双击时,如果鼠标位置与最后一个点不同,先添加当前点
|
||||
handleAddPoint()
|
||||
if (featurePositions.length < 3) {
|
||||
// TODO: 替换为实际的错误提示方式
|
||||
console.error('请保证攻击箭头有至少3个特征点!')
|
||||
return
|
||||
}
|
||||
dispose()
|
||||
// 使用 setTimeout 避免立即 resolve 可能导致的事件冲突或其他问题
|
||||
setTimeout(() => {
|
||||
resolve(featurePositions)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// 右键取消绘制或删除最后一个点
|
||||
const handleCancelDraw = () => {
|
||||
if (featurePositions.length > 0) {
|
||||
// 取消最后绘制的点
|
||||
featurePositions.pop()
|
||||
} else {
|
||||
// 如果没有点,则取消整个绘制
|
||||
abort()
|
||||
}
|
||||
}
|
||||
|
||||
// 注册当前的取消回调
|
||||
registerAbort(abort)
|
||||
|
||||
// 绑定绘制相关事件
|
||||
bus.onScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint)
|
||||
bus.onScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw)
|
||||
bus.onScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
import {
|
||||
Cartesian3,
|
||||
Entity,
|
||||
CallbackProperty,
|
||||
Color as CesiumColor,
|
||||
ScreenSpaceEventType,
|
||||
PolygonHierarchy,
|
||||
HeightReference,
|
||||
} from 'cesium'
|
||||
import { createCurvePolygonPositions } from './draw/curvePolygonGraphic'
|
||||
|
||||
/**
|
||||
* 绘制多边形的具体逻辑
|
||||
* @param deps 绘制所需的依赖
|
||||
* @returns Promise 绘制完成的所有点坐标数组
|
||||
*/
|
||||
export const drawCurvePolygon = (deps) => {
|
||||
const { viewer, bus, position, status, registerAbort, globalAbort } = deps
|
||||
|
||||
// 如果在执行其它操作,则取消
|
||||
globalAbort()
|
||||
|
||||
status.value = 'Drawing'
|
||||
const featurePositions = []
|
||||
|
||||
// 临时多边形实体 (包含填充和边界线)
|
||||
const tempPolygon = new Entity({
|
||||
name: '__draw_temp_curve_polygon',
|
||||
polygon: {
|
||||
hierarchy: new CallbackProperty(() => {
|
||||
const positions = [...featurePositions]
|
||||
// 添加当前鼠标位置作为最后一个点,形成动态效果
|
||||
if (
|
||||
position.value &&
|
||||
!position.value.equals(featurePositions[featurePositions.length - 1]) // 避免重复添加同一个点
|
||||
) {
|
||||
positions.push(position.value)
|
||||
}
|
||||
// 曲面需要至少2个点才能形成
|
||||
if (positions.length < 2) {
|
||||
return null
|
||||
}
|
||||
return new PolygonHierarchy(createCurvePolygonPositions(positions))
|
||||
}, false),
|
||||
heightReference: HeightReference.CLAMP_TO_GROUND,
|
||||
material: CesiumColor.fromCssColorString('#ff0000').withAlpha(0.3),
|
||||
},
|
||||
// 绘制边界线,连接所有点和第一个点,形成闭合效果
|
||||
polyline: {
|
||||
positions: new CallbackProperty(() => {
|
||||
const positions = [...featurePositions]
|
||||
// 添加当前鼠标位置作为最后一个点
|
||||
if (
|
||||
position.value &&
|
||||
!position.value.equals(featurePositions[featurePositions.length - 1])
|
||||
) {
|
||||
positions.push(position.value)
|
||||
}
|
||||
// 曲面需要至少2个点才能形成
|
||||
if (positions.length < 2) {
|
||||
return null
|
||||
}
|
||||
return createCurvePolygonPositions(positions)
|
||||
}, false),
|
||||
// clampToGround: true, // 多边形边界线通常不需要 clampToGround,因为多边形本身已经处理了高度
|
||||
width: 2,
|
||||
material: CesiumColor.fromCssColorString('#ff0000'),
|
||||
},
|
||||
})
|
||||
|
||||
viewer.entities.add(tempPolygon)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// 清理资源
|
||||
const dispose = () => {
|
||||
registerAbort(null) // 取消注册当前的 abort 回调
|
||||
status.value = 'Default'
|
||||
viewer.entities.remove(tempPolygon)
|
||||
bus.offScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint)
|
||||
bus.offScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw)
|
||||
bus.offScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw)
|
||||
}
|
||||
|
||||
// 取消绘制
|
||||
const abort = (info) => {
|
||||
dispose()
|
||||
reject(info || '取消曲面绘制!')
|
||||
}
|
||||
|
||||
// 左键加点
|
||||
const handleAddPoint = () => {
|
||||
if (position.value && !position.value.equals(featurePositions[featurePositions.length - 1])) {
|
||||
featurePositions.push(position.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 左键双击完成绘制
|
||||
const handleEndDraw = () => {
|
||||
// 双击时,如果鼠标位置与最后一个点不同,先添加当前点
|
||||
handleAddPoint()
|
||||
if (featurePositions.length < 2) {
|
||||
// TODO: 替换为实际的错误提示方式
|
||||
console.error('请保曲面至少有2个特征点!')
|
||||
return
|
||||
}
|
||||
dispose()
|
||||
// 使用 setTimeout 避免立即 resolve 可能导致的事件冲突或其他问题
|
||||
setTimeout(() => {
|
||||
resolve(featurePositions)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// 右键取消绘制或删除最后一个点
|
||||
const handleCancelDraw = () => {
|
||||
if (featurePositions.length > 0) {
|
||||
// 取消最后绘制的点
|
||||
featurePositions.pop()
|
||||
} else {
|
||||
// 如果没有点,则取消整个绘制
|
||||
abort()
|
||||
}
|
||||
}
|
||||
|
||||
// 注册当前的取消回调
|
||||
registerAbort(abort)
|
||||
|
||||
// 绑定绘制相关事件
|
||||
bus.onScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint)
|
||||
bus.onScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw)
|
||||
bus.onScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
import {
|
||||
Cartesian3,
|
||||
Entity,
|
||||
CallbackProperty,
|
||||
Color as CesiumColor,
|
||||
ScreenSpaceEventType,
|
||||
PolygonHierarchy,
|
||||
HeightReference,
|
||||
} from 'cesium'
|
||||
import { createDoubleArrowPositions } from './draw/doubleArrowGraphic'
|
||||
|
||||
/**
|
||||
* 绘制双箭头的具体逻辑
|
||||
* @param deps 绘制所需的依赖
|
||||
* @returns Promise 绘制完成的所有点坐标数组
|
||||
*/
|
||||
export const drawDoubleArrow = (deps) => {
|
||||
const { viewer, bus, position, status, registerAbort, globalAbort } = deps
|
||||
|
||||
// 如果在执行其它操作,则取消
|
||||
globalAbort()
|
||||
|
||||
status.value = 'Drawing'
|
||||
const featurePositions = []
|
||||
|
||||
// 临时多边形实体 (包含填充和边界线)
|
||||
const tempPolygon = new Entity({
|
||||
name: '__draw_temp_double_arrow',
|
||||
polygon: {
|
||||
hierarchy: new CallbackProperty(() => {
|
||||
const positions = [...featurePositions]
|
||||
// 添加当前鼠标位置作为最后一个点,形成动态效果
|
||||
if (
|
||||
position.value &&
|
||||
!position.value.equals(featurePositions[featurePositions.length - 1]) // 避免重复添加同一个点
|
||||
) {
|
||||
positions.push(position.value)
|
||||
}
|
||||
// 双箭头需要至少3个点形成
|
||||
if (positions.length < 3) {
|
||||
return null
|
||||
}
|
||||
return new PolygonHierarchy(createDoubleArrowPositions(positions))
|
||||
}, false),
|
||||
heightReference: HeightReference.CLAMP_TO_GROUND,
|
||||
material: CesiumColor.fromCssColorString('#ff0000').withAlpha(0.3),
|
||||
},
|
||||
// 绘制边界线,连接所有点和第一个点,形成闭合效果
|
||||
polyline: {
|
||||
positions: new CallbackProperty(() => {
|
||||
const positions = [...featurePositions]
|
||||
// 添加当前鼠标位置作为最后一个点
|
||||
if (
|
||||
position.value &&
|
||||
!position.value.equals(featurePositions[featurePositions.length - 1])
|
||||
) {
|
||||
positions.push(position.value)
|
||||
}
|
||||
// 双箭头需要至少2个点才能显示线
|
||||
if (positions.length < 2) {
|
||||
return null
|
||||
}
|
||||
// 只有2个点时,显示箭头尾部的线
|
||||
if (positions.length === 2) {
|
||||
return positions
|
||||
}
|
||||
const pts = createDoubleArrowPositions(positions)
|
||||
return [...pts, pts[0]]
|
||||
}, false),
|
||||
// clampToGround: true, // 多边形边界线通常不需要 clampToGround,因为多边形本身已经处理了高度
|
||||
width: 2,
|
||||
material: CesiumColor.fromCssColorString('#ff0000'),
|
||||
},
|
||||
})
|
||||
|
||||
viewer.entities.add(tempPolygon)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// 清理资源
|
||||
const dispose = () => {
|
||||
registerAbort(null) // 取消注册当前的 abort 回调
|
||||
status.value = 'Default'
|
||||
viewer.entities.remove(tempPolygon)
|
||||
bus.offScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint)
|
||||
bus.offScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw)
|
||||
bus.offScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw)
|
||||
}
|
||||
|
||||
// 取消绘制
|
||||
const abort = (info) => {
|
||||
dispose()
|
||||
reject(info || '取消双箭头绘制!')
|
||||
}
|
||||
|
||||
// 左键加点
|
||||
const handleAddPoint = () => {
|
||||
if (position.value && !position.value.equals(featurePositions[featurePositions.length - 1])) {
|
||||
featurePositions.push(position.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 左键双击完成绘制
|
||||
const handleEndDraw = () => {
|
||||
// 双击时,如果鼠标位置与最后一个点不同,先添加当前点
|
||||
handleAddPoint()
|
||||
if (featurePositions.length < 3) {
|
||||
// TODO: 替换为实际的错误提示方式
|
||||
console.error('请保证双箭头有至少3个特征点!')
|
||||
return
|
||||
}
|
||||
dispose()
|
||||
// 使用 setTimeout 避免立即 resolve 可能导致的事件冲突或其他问题
|
||||
setTimeout(() => {
|
||||
resolve(featurePositions)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// 右键取消绘制或删除最后一个点
|
||||
const handleCancelDraw = () => {
|
||||
if (featurePositions.length > 0) {
|
||||
// 取消最后绘制的点
|
||||
featurePositions.pop()
|
||||
} else {
|
||||
// 如果没有点,则取消整个绘制
|
||||
abort()
|
||||
}
|
||||
}
|
||||
|
||||
// 注册当前的取消回调
|
||||
registerAbort(abort)
|
||||
|
||||
// 绑定绘制相关事件
|
||||
bus.onScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint)
|
||||
bus.onScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw)
|
||||
bus.onScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import { Cartesian3, ScreenSpaceEventType } from 'cesium'
|
||||
|
||||
/**
|
||||
* 绘制点的具体逻辑
|
||||
* @param deps 绘制所需的依赖
|
||||
* @returns Promise 绘制完成的点坐标
|
||||
*/
|
||||
export const drawPoint = (deps) => {
|
||||
const { bus, position, status, registerAbort, globalAbort } = deps
|
||||
|
||||
// 如果在执行其它操作,则取消
|
||||
globalAbort()
|
||||
|
||||
status.value = 'Drawing'
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// 清理资源
|
||||
const dispose = () => {
|
||||
registerAbort(null) // 取消注册当前的 abort 回调
|
||||
status.value = 'Default'
|
||||
bus.offScreen(ScreenSpaceEventType.LEFT_CLICK, handleDraw)
|
||||
bus.offScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancel)
|
||||
}
|
||||
|
||||
// 取消绘制
|
||||
const abort = (info) => {
|
||||
dispose()
|
||||
reject(info || '取消点绘制!')
|
||||
}
|
||||
|
||||
// 左键绘制
|
||||
const handleDraw = () => {
|
||||
if (!position.value) {
|
||||
console.error('坐标信息转换失败,获取的世界坐标异常!')
|
||||
return
|
||||
}
|
||||
const p = position.value
|
||||
dispose()
|
||||
|
||||
// 使用 setTimeout 避免立即 resolve 可能导致的事件冲突或其他问题
|
||||
setTimeout(() => {
|
||||
resolve(p)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// 右键取消
|
||||
const handleCancel = () => abort()
|
||||
|
||||
// 注册当前的取消回调
|
||||
registerAbort(abort)
|
||||
|
||||
// 绑定绘制相关事件
|
||||
bus.onScreen(ScreenSpaceEventType.LEFT_CLICK, handleDraw)
|
||||
bus.onScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancel)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
import {
|
||||
Cartesian3,
|
||||
Entity,
|
||||
CallbackProperty,
|
||||
Color as CesiumColor,
|
||||
ScreenSpaceEventType,
|
||||
PolygonHierarchy,
|
||||
HeightReference,
|
||||
} from 'cesium'
|
||||
|
||||
/**
|
||||
* 绘制多边形的具体逻辑
|
||||
* @param deps 绘制所需的依赖
|
||||
* @returns Promise 绘制完成的所有点坐标数组
|
||||
*/
|
||||
export const drawPolygon = (deps) => {
|
||||
const { viewer, bus, position, status, registerAbort, globalAbort } = deps
|
||||
|
||||
// 如果在执行其它操作,则取消
|
||||
globalAbort()
|
||||
|
||||
status.value = 'Drawing'
|
||||
const featurePositions = []
|
||||
|
||||
// 临时多边形实体 (包含填充和边界线)
|
||||
const tempPolygon = new Entity({
|
||||
name: '__draw_temp_polygon',
|
||||
polygon: {
|
||||
hierarchy: new CallbackProperty(() => {
|
||||
const positions = [...featurePositions]
|
||||
// 添加当前鼠标位置作为最后一个点,形成动态效果
|
||||
if (
|
||||
position.value &&
|
||||
!position.value.equals(featurePositions[featurePositions.length - 1]) // 避免重复添加同一个点
|
||||
) {
|
||||
positions.push(position.value)
|
||||
}
|
||||
// 多边形需要至少3个点才能形成
|
||||
if (positions.length < 3) {
|
||||
return null
|
||||
}
|
||||
return new PolygonHierarchy(positions)
|
||||
}, false),
|
||||
heightReference: HeightReference.CLAMP_TO_GROUND,
|
||||
material: CesiumColor.fromCssColorString('#ff0000').withAlpha(0.3),
|
||||
},
|
||||
// 绘制边界线,连接所有点和第一个点,形成闭合效果
|
||||
polyline: {
|
||||
positions: new CallbackProperty(() => {
|
||||
const positions = [...featurePositions]
|
||||
// 添加当前鼠标位置作为最后一个点
|
||||
if (
|
||||
position.value &&
|
||||
!position.value.equals(featurePositions[featurePositions.length - 1])
|
||||
) {
|
||||
positions.push(position.value)
|
||||
}
|
||||
// 如果有至少一个点,则连接到第一个点形成闭合线
|
||||
if (featurePositions.length > 0 && featurePositions[0]) {
|
||||
positions.push(featurePositions[0])
|
||||
}
|
||||
return positions
|
||||
}, false),
|
||||
// clampToGround: true, // 多边形边界线通常不需要 clampToGround,因为多边形本身已经处理了高度
|
||||
width: 2,
|
||||
material: CesiumColor.fromCssColorString('#ff0000'),
|
||||
},
|
||||
})
|
||||
|
||||
viewer.entities.add(tempPolygon)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// 清理资源
|
||||
const dispose = () => {
|
||||
registerAbort(null) // 取消注册当前的 abort 回调
|
||||
status.value = 'Default'
|
||||
viewer.entities.remove(tempPolygon)
|
||||
bus.offScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint)
|
||||
bus.offScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw)
|
||||
bus.offScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw)
|
||||
}
|
||||
|
||||
// 取消绘制
|
||||
const abort = (info) => {
|
||||
dispose()
|
||||
reject(info || '取消多边形绘制!')
|
||||
}
|
||||
|
||||
// 左键加点
|
||||
const handleAddPoint = () => {
|
||||
if (position.value && !position.value.equals(featurePositions[featurePositions.length - 1])) {
|
||||
featurePositions.push(position.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 左键双击完成绘制
|
||||
const handleEndDraw = () => {
|
||||
// 双击时,如果鼠标位置与最后一个点不同,先添加当前点
|
||||
handleAddPoint()
|
||||
if (featurePositions.length < 3) {
|
||||
// TODO: 替换为实际的错误提示方式
|
||||
console.error('请保多边形至少有三个点!')
|
||||
return
|
||||
}
|
||||
dispose()
|
||||
// 使用 setTimeout 避免立即 resolve 可能导致的事件冲突或其他问题
|
||||
setTimeout(() => {
|
||||
resolve(featurePositions)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// 右键取消绘制或删除最后一个点
|
||||
const handleCancelDraw = () => {
|
||||
if (featurePositions.length > 0) {
|
||||
// 取消最后绘制的点
|
||||
featurePositions.pop()
|
||||
} else {
|
||||
// 如果没有点,则取消整个绘制
|
||||
abort()
|
||||
}
|
||||
}
|
||||
|
||||
// 注册当前的取消回调
|
||||
registerAbort(abort)
|
||||
|
||||
// 绑定绘制相关事件
|
||||
bus.onScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint)
|
||||
bus.onScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw)
|
||||
bus.onScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
import {
|
||||
Cartesian3,
|
||||
Entity,
|
||||
CallbackProperty,
|
||||
Color as CesiumColor,
|
||||
ScreenSpaceEventType,
|
||||
} from 'cesium'
|
||||
|
||||
/**
|
||||
* 绘制折线的具体逻辑
|
||||
* @param deps 绘制所需的依赖
|
||||
* @returns Promise 绘制完成的所有点坐标数组
|
||||
*/
|
||||
export const drawPolyline = (deps) => {
|
||||
const { viewer, bus, position, status, registerAbort, globalAbort } = deps
|
||||
|
||||
// 如果在执行其它操作,则取消
|
||||
globalAbort()
|
||||
|
||||
status.value = 'Drawing'
|
||||
const featurePositions = []
|
||||
|
||||
// 临时折线实体
|
||||
const tempPolyline = new Entity({
|
||||
name: '__draw_temp_polyline',
|
||||
polyline: {
|
||||
positions: new CallbackProperty(() => {
|
||||
const positions = [...featurePositions]
|
||||
// 添加当前鼠标位置作为最后一个点,形成动态效果
|
||||
if (
|
||||
position.value &&
|
||||
!position.value.equals(featurePositions[featurePositions.length - 1]) // 避免重复添加同一个点
|
||||
) {
|
||||
positions.push(position.value)
|
||||
}
|
||||
return positions
|
||||
}, false),
|
||||
width: 2,
|
||||
material: CesiumColor.fromCssColorString('#FF0000'),
|
||||
clampToGround: true,
|
||||
},
|
||||
})
|
||||
|
||||
viewer.entities.add(tempPolyline)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// 清理资源
|
||||
const dispose = () => {
|
||||
registerAbort(null) // 取消注册当前的 abort 回调
|
||||
status.value = 'Default'
|
||||
viewer.entities.remove(tempPolyline)
|
||||
bus.offScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint)
|
||||
bus.offScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw)
|
||||
bus.offScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw)
|
||||
}
|
||||
|
||||
// 取消绘制
|
||||
const abort = (info) => {
|
||||
dispose()
|
||||
reject(info || '取消折线绘制!')
|
||||
}
|
||||
|
||||
// 左键加点
|
||||
const handleAddPoint = () => {
|
||||
if (position.value && !position.value.equals(featurePositions[featurePositions.length - 1])) {
|
||||
featurePositions.push(position.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 左键双击完成绘制
|
||||
const handleEndDraw = () => {
|
||||
// 双击时,如果鼠标位置与最后一个点不同,先添加当前点
|
||||
handleAddPoint()
|
||||
if (featurePositions.length < 2) {
|
||||
// TODO: 替换为实际的错误提示方式
|
||||
console.error('请保证折线至少有两个点!')
|
||||
return
|
||||
}
|
||||
dispose()
|
||||
// 使用 setTimeout 避免立即 resolve 可能导致的事件冲突或其他问题
|
||||
setTimeout(() => {
|
||||
resolve(featurePositions)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// 右键取消绘制或删除最后一个点
|
||||
const handleCancelDraw = () => {
|
||||
if (featurePositions.length > 0) {
|
||||
// 取消最后绘制的点
|
||||
featurePositions.pop()
|
||||
} else {
|
||||
// 如果没有点,则取消整个绘制
|
||||
abort()
|
||||
}
|
||||
}
|
||||
|
||||
// 注册当前的取消回调
|
||||
registerAbort(abort)
|
||||
|
||||
// 绑定绘制相关事件
|
||||
bus.onScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint)
|
||||
bus.onScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw)
|
||||
bus.onScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
import {
|
||||
Cartesian3,
|
||||
Entity,
|
||||
CallbackProperty,
|
||||
Color as CesiumColor,
|
||||
ScreenSpaceEventType,
|
||||
PolygonHierarchy,
|
||||
HeightReference,
|
||||
} from 'cesium'
|
||||
import { createStraightArrowPositions } from './draw/straightArrowGraphic'
|
||||
|
||||
/**
|
||||
* 绘制直箭头的具体逻辑
|
||||
* @param deps 绘制所需的依赖
|
||||
* @returns Promise 绘制完成的所有点坐标数组
|
||||
*/
|
||||
export const drawStraightArrow = (deps) => {
|
||||
const { viewer, bus, position, status, registerAbort, globalAbort } = deps
|
||||
|
||||
// 如果在执行其它操作,则取消
|
||||
globalAbort()
|
||||
|
||||
status.value = 'Drawing'
|
||||
const featurePositions = []
|
||||
|
||||
// 临时多边形实体 (包含填充和边界线)
|
||||
const tempPolygon = new Entity({
|
||||
name: '__draw_temp_straight_arrow',
|
||||
polygon: {
|
||||
hierarchy: new CallbackProperty(() => {
|
||||
const positions = [...featurePositions]
|
||||
// 添加当前鼠标位置作为最后一个点,形成动态效果
|
||||
if (
|
||||
position.value &&
|
||||
!position.value.equals(featurePositions[featurePositions.length - 1]) // 避免重复添加同一个点
|
||||
) {
|
||||
positions.push(position.value)
|
||||
}
|
||||
// 直箭头需要2个点形成
|
||||
if (positions.length < 2) {
|
||||
return null
|
||||
}
|
||||
return new PolygonHierarchy(createStraightArrowPositions(positions))
|
||||
}, false),
|
||||
heightReference: HeightReference.CLAMP_TO_GROUND,
|
||||
material: CesiumColor.fromCssColorString('#ff0000').withAlpha(0.3),
|
||||
},
|
||||
// 绘制边界线,连接所有点和第一个点,形成闭合效果
|
||||
polyline: {
|
||||
positions: new CallbackProperty(() => {
|
||||
const positions = [...featurePositions]
|
||||
// 添加当前鼠标位置作为最后一个点
|
||||
if (
|
||||
position.value &&
|
||||
!position.value.equals(featurePositions[featurePositions.length - 1])
|
||||
) {
|
||||
positions.push(position.value)
|
||||
}
|
||||
// 直箭头需要2个点形成
|
||||
if (positions.length < 2) {
|
||||
return null
|
||||
}
|
||||
const pts = createStraightArrowPositions(positions)
|
||||
return [...pts, pts[0]]
|
||||
}, false),
|
||||
// clampToGround: true, // 多边形边界线通常不需要 clampToGround,因为多边形本身已经处理了高度
|
||||
width: 2,
|
||||
material: CesiumColor.fromCssColorString('#ff0000'),
|
||||
},
|
||||
})
|
||||
|
||||
viewer.entities.add(tempPolygon)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// 清理资源
|
||||
const dispose = () => {
|
||||
registerAbort(null) // 取消注册当前的 abort 回调
|
||||
status.value = 'Default'
|
||||
viewer.entities.remove(tempPolygon)
|
||||
bus.offScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint)
|
||||
bus.offScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw)
|
||||
bus.offScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw)
|
||||
}
|
||||
|
||||
// 取消绘制
|
||||
const abort = (info) => {
|
||||
dispose()
|
||||
reject(info || '取消曲面绘制!')
|
||||
}
|
||||
|
||||
// 左键加点
|
||||
const handleAddPoint = () => {
|
||||
// 直箭头最多绘制2个点
|
||||
if (featurePositions.length >= 2) return
|
||||
|
||||
if (position.value && !position.value.equals(featurePositions[featurePositions.length - 1])) {
|
||||
featurePositions.push(position.value)
|
||||
|
||||
// 如果达到2个点,自动完成绘制
|
||||
if (featurePositions.length === 2) {
|
||||
dispose()
|
||||
setTimeout(() => {
|
||||
featurePositions[0] &&
|
||||
featurePositions[1] &&
|
||||
resolve([featurePositions[0], featurePositions[1]])
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 左键双击完成绘制
|
||||
const handleEndDraw = () => {
|
||||
// 双击时,如果鼠标位置与最后一个点不同,先添加当前点
|
||||
handleAddPoint()
|
||||
if (featurePositions.length < 2) {
|
||||
// TODO: 替换为实际的错误提示方式
|
||||
console.error('请保证直箭头有2个特征点!')
|
||||
return
|
||||
}
|
||||
dispose()
|
||||
// 使用 setTimeout 避免立即 resolve 可能导致的事件冲突或其他问题
|
||||
setTimeout(() => {
|
||||
featurePositions[0] &&
|
||||
featurePositions[1] &&
|
||||
resolve([featurePositions[0], featurePositions[1]])
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// 右键取消绘制或删除最后一个点
|
||||
const handleCancelDraw = () => {
|
||||
if (featurePositions.length > 0) {
|
||||
// 取消最后绘制的点
|
||||
featurePositions.pop()
|
||||
} else {
|
||||
// 如果没有点,则取消整个绘制
|
||||
abort()
|
||||
}
|
||||
}
|
||||
|
||||
// 注册当前的取消回调
|
||||
registerAbort(abort)
|
||||
|
||||
// 绑定绘制相关事件
|
||||
bus.onScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint)
|
||||
bus.onScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw)
|
||||
bus.onScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
import {
|
||||
Cartesian3,
|
||||
Entity,
|
||||
CallbackProperty,
|
||||
Color as CesiumColor,
|
||||
ScreenSpaceEventType,
|
||||
PolygonHierarchy,
|
||||
HeightReference,
|
||||
} from 'cesium'
|
||||
import { createWideArrowPositions } from './draw/wideArrowGraphic'
|
||||
|
||||
/**
|
||||
* 绘制宽箭头的具体逻辑
|
||||
* @param deps 绘制所需的依赖
|
||||
* @returns Promise 绘制完成的所有点坐标数组
|
||||
*/
|
||||
export const drawWideArrow = (deps) => {
|
||||
const { viewer, bus, position, status, registerAbort, globalAbort } = deps
|
||||
|
||||
// 如果在执行其它操作,则取消
|
||||
globalAbort()
|
||||
|
||||
status.value = 'Drawing'
|
||||
const featurePositions = []
|
||||
|
||||
// 临时多边形实体 (包含填充和边界线)
|
||||
const tempPolygon = new Entity({
|
||||
name: '__draw_temp_wide_arrow',
|
||||
polygon: {
|
||||
hierarchy: new CallbackProperty(() => {
|
||||
const positions = [...featurePositions]
|
||||
// 添加当前鼠标位置作为最后一个点,形成动态效果
|
||||
if (
|
||||
position.value &&
|
||||
!position.value.equals(featurePositions[featurePositions.length - 1]) // 避免重复添加同一个点
|
||||
) {
|
||||
positions.push(position.value)
|
||||
}
|
||||
// 宽箭头需要至少2个点形成
|
||||
if (positions.length < 2) {
|
||||
return null
|
||||
}
|
||||
return new PolygonHierarchy(createWideArrowPositions(positions))
|
||||
}, false),
|
||||
heightReference: HeightReference.CLAMP_TO_GROUND,
|
||||
material: CesiumColor.fromCssColorString('#ff0000').withAlpha(0.3),
|
||||
},
|
||||
// 绘制边界线,连接所有点和第一个点,形成闭合效果
|
||||
polyline: {
|
||||
positions: new CallbackProperty(() => {
|
||||
const positions = [...featurePositions]
|
||||
// 添加当前鼠标位置作为最后一个点
|
||||
if (
|
||||
position.value &&
|
||||
!position.value.equals(featurePositions[featurePositions.length - 1])
|
||||
) {
|
||||
positions.push(position.value)
|
||||
}
|
||||
// 宽箭头需要至少2个点形成
|
||||
if (positions.length < 2) {
|
||||
return null
|
||||
}
|
||||
const pts = createWideArrowPositions(positions)
|
||||
return [...pts, pts[0]]
|
||||
// return [...pts]
|
||||
}, false),
|
||||
// clampToGround: true, // 多边形边界线通常不需要 clampToGround,因为多边形本身已经处理了高度
|
||||
width: 2,
|
||||
material: CesiumColor.fromCssColorString('#ff0000'),
|
||||
},
|
||||
})
|
||||
|
||||
viewer.entities.add(tempPolygon)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// 清理资源
|
||||
const dispose = () => {
|
||||
registerAbort(null) // 取消注册当前的 abort 回调
|
||||
status.value = 'Default'
|
||||
viewer.entities.remove(tempPolygon)
|
||||
bus.offScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint)
|
||||
bus.offScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw)
|
||||
bus.offScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw)
|
||||
}
|
||||
|
||||
// 取消绘制
|
||||
const abort = (info) => {
|
||||
dispose()
|
||||
reject(info || '取消宽箭头绘制!')
|
||||
}
|
||||
|
||||
// 左键加点
|
||||
const handleAddPoint = () => {
|
||||
if (position.value && !position.value.equals(featurePositions[featurePositions.length - 1])) {
|
||||
featurePositions.push(position.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 左键双击完成绘制
|
||||
const handleEndDraw = () => {
|
||||
// 双击时,如果鼠标位置与最后一个点不同,先添加当前点
|
||||
handleAddPoint()
|
||||
if (featurePositions.length < 2) {
|
||||
// TODO: 替换为实际的错误提示方式
|
||||
console.error('请保证宽箭头有至少2个特征点!')
|
||||
return
|
||||
}
|
||||
dispose()
|
||||
// 使用 setTimeout 避免立即 resolve 可能导致的事件冲突或其他问题
|
||||
setTimeout(() => {
|
||||
resolve(featurePositions)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// 右键取消绘制或删除最后一个点
|
||||
const handleCancelDraw = () => {
|
||||
if (featurePositions.length > 0) {
|
||||
// 取消最后绘制的点
|
||||
featurePositions.pop()
|
||||
} else {
|
||||
// 如果没有点,则取消整个绘制
|
||||
abort()
|
||||
}
|
||||
}
|
||||
|
||||
// 注册当前的取消回调
|
||||
registerAbort(abort)
|
||||
|
||||
// 绑定绘制相关事件
|
||||
bus.onScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint)
|
||||
bus.onScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw)
|
||||
bus.onScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
import { ref, watch } from 'vue'
|
||||
import {
|
||||
Viewer,
|
||||
Cartesian3,
|
||||
CustomDataSource,
|
||||
Entity,
|
||||
CallbackPositionProperty,
|
||||
Color as CesiumColor,
|
||||
} from 'cesium'
|
||||
import { useEventBus } from '../useEventBus'
|
||||
import { useHoverPosition } from '../useHoverPosition'
|
||||
|
||||
// 导入拆分后的类型和绘制逻辑
|
||||
import { drawPoint as _drawPoint } from './drawPoint'
|
||||
import { drawPolyline as _drawPolyline } from './drawPolyline'
|
||||
import { drawPolygon as _drawPolygon } from './drawPolygon'
|
||||
import { drawCurvePolygon as _drawCurvePolygon } from './drawCurvePolygon'
|
||||
import { drawStraightArrow as _drawStraightArrow } from './drawStraightArrow'
|
||||
import { drawWideArrow as _drawWideArrow } from './drawWideArrow'
|
||||
import { drawAttackArrow as _drawAttackArrow } from './drawAttackArrow'
|
||||
import { drawDoubleArrow as _drawDoubleArrow } from './drawDoubleArrow'
|
||||
|
||||
import { createCurvePolygonPositions } from './draw/curvePolygonGraphic'
|
||||
import { createStraightArrowPositions } from './draw/straightArrowGraphic'
|
||||
import { createWideArrowPositions } from './draw/wideArrowGraphic'
|
||||
import { createAttackArrowPositions } from './draw/attackArrowGraphic'
|
||||
import { createDoubleArrowPositions } from './draw/doubleArrowGraphic'
|
||||
|
||||
// 所有useDrawTool实例共用一个绘制状态和取消回调
|
||||
// 如果需要每个实例独立,则将此变量移入 useDrawTool 函数内部
|
||||
let abortCallback = null
|
||||
|
||||
// 存储编辑状态辅助entity的图层
|
||||
let assistLayer = null
|
||||
|
||||
export const useDrawTool = (viewer) => {
|
||||
// 获取实时位置
|
||||
const { isHover, position } = useHoverPosition(viewer)
|
||||
// 获取事件总线
|
||||
const bus = useEventBus(viewer)
|
||||
const status = ref('Default')
|
||||
|
||||
// 判断辅助点是否显示
|
||||
watch(
|
||||
() => [status.value, isHover.value],
|
||||
([nowStatus, hover]) => {
|
||||
if (hover && nowStatus === 'Drawing') {
|
||||
assistPoint.show = true
|
||||
} else {
|
||||
assistPoint.show = false
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if (!assistLayer) {
|
||||
assistLayer = new CustomDataSource('__assist_edit')
|
||||
viewer.dataSources.add(assistLayer)
|
||||
}
|
||||
|
||||
// 辅助点,用于显示获取的实际位置
|
||||
const assistPoint = new Entity({
|
||||
name: '__draw_icon',
|
||||
show: false,
|
||||
position: new CallbackPositionProperty(() => {
|
||||
return position.value || undefined
|
||||
}, false),
|
||||
point: {
|
||||
pixelSize: 7,
|
||||
color: CesiumColor.BLUE,
|
||||
outlineColor: CesiumColor.WHITE,
|
||||
outlineWidth: 1,
|
||||
},
|
||||
// 可以考虑是否需要 HeightReference.CLAMP_TO_GROUND
|
||||
// heightReference: HeightReference.CLAMP_TO_GROUND,
|
||||
})
|
||||
viewer.entities.add(assistPoint)
|
||||
|
||||
// 全局取消当前绘制操作的函数
|
||||
const abort = () => {
|
||||
if (abortCallback) {
|
||||
abortCallback() // 调用当前注册的取消回调
|
||||
abortCallback = null // 清空回调
|
||||
}
|
||||
}
|
||||
|
||||
// 注册当前绘制操作的取消回调函数
|
||||
const registerAbort = (cb) => {
|
||||
abortCallback = cb
|
||||
}
|
||||
|
||||
// 准备传递给绘制函数的依赖对象
|
||||
const drawDependencies = {
|
||||
viewer,
|
||||
bus,
|
||||
position,
|
||||
status,
|
||||
registerAbort,
|
||||
globalAbort: abort, // 将全局 abort 函数也传递进去,方便内部调用
|
||||
}
|
||||
|
||||
// 调用拆分后的绘制函数,并传递依赖
|
||||
const drawPoint = () => _drawPoint(drawDependencies)
|
||||
const drawPolyline = () => _drawPolyline(drawDependencies)
|
||||
const drawPolygon = () => _drawPolygon(drawDependencies)
|
||||
const drawCurvePolygon = () => _drawCurvePolygon(drawDependencies)
|
||||
const drawStraightArrow = () => _drawStraightArrow(drawDependencies)
|
||||
const drawWideArrow = () => _drawWideArrow(drawDependencies)
|
||||
const drawAttackArrow = () => _drawAttackArrow(drawDependencies)
|
||||
const drawDoubleArrow = () => _drawDoubleArrow(drawDependencies)
|
||||
|
||||
// TODO: 添加 startEdit 等其他编辑功能
|
||||
// 在 hook 卸载时清理资源 (如果 hook 实例会被销毁)
|
||||
// Vue 3 Composition API 的 setup 函数返回的对象没有 beforeDestroy/unmounted 生命周期钩子
|
||||
// 如果 useDrawTool 是在 setup 中调用,并且 setup 返回了 useDrawTool 的结果,
|
||||
// 那么当组件卸载时,Cesium 实体和事件监听可能不会自动清理。
|
||||
// 需要手动处理清理逻辑,例如在组件的 onUnmounted 钩子中调用一个 cleanup 函数。
|
||||
// 或者 useEventBus 内部已经处理了 Viewer 销毁时的清理。
|
||||
// 这里暂时不添加清理逻辑,假设 useEventBus 和 useHoverPosition 已经处理或外部会手动清理。
|
||||
// 如果需要,可以在这里返回一个 cleanup 函数,并在组件 unmounted 时调用。
|
||||
/*
|
||||
const cleanup = () => {
|
||||
abort(); // 取消所有正在进行的绘制
|
||||
viewer.dataSources.remove(assistLayer, true); // 移除并销毁数据源
|
||||
viewer.entities.remove(assistPoint); // 移除辅助点
|
||||
// useEventBus 和 useHoverPosition 可能也需要清理
|
||||
};
|
||||
// return { ..., cleanup };
|
||||
*/
|
||||
|
||||
return {
|
||||
drawPoint,
|
||||
drawPolyline,
|
||||
drawPolygon,
|
||||
drawCurvePolygon,
|
||||
drawStraightArrow,
|
||||
drawWideArrow,
|
||||
drawAttackArrow,
|
||||
drawDoubleArrow,
|
||||
abort,
|
||||
// 如果需要,可以暴露 status
|
||||
// status
|
||||
}
|
||||
}
|
||||
|
||||
export { createCurvePolygonPositions, createStraightArrowPositions, createWideArrowPositions, createAttackArrowPositions, createDoubleArrowPositions }
|
|
@ -0,0 +1,127 @@
|
|||
import mitt from 'mitt'
|
||||
import { Viewer, ScreenSpaceEventHandler, ScreenSpaceEventType } from 'cesium'
|
||||
|
||||
|
||||
// 使用 Map 来缓存每个 viewer 对应的事件总线组件
|
||||
// Viewer 实例作为 Map 的键
|
||||
const viewerEventBusCache = new Map()
|
||||
|
||||
/**
|
||||
* 消息总线,接管全局屏幕空间事件,仅作用于单个 Viewer。
|
||||
* 调用该hook后,所有鼠标事件必须改为调用返回对象中的方法。
|
||||
* 使用方法参考发布订阅模式,Cesium.ScreenSpaceEventType的订阅/解订用onScreen/offScreen,自定义事件用on/off。
|
||||
* 注意,这里不单独划分带键盘按键的鼠标事件,而是到具体事件中划分。
|
||||
* @param viewer Cesium Viewer 实例
|
||||
* @returns EventBus 对象
|
||||
*/
|
||||
export const useEventBus = (viewer) => {
|
||||
let cache = viewerEventBusCache.get(viewer)
|
||||
if (!cache) {
|
||||
cache = {
|
||||
// 创建一个 mitt 实例用于处理所有屏幕空间事件
|
||||
screenEmitter: mitt(),
|
||||
// 创建一个 mitt 实例用于处理自定义事件
|
||||
customEmitter: mitt(),
|
||||
// 存储创建的 ScreenSpaceEventHandler 实例,以便销毁
|
||||
// 注意:这里没有使用viewer中自带的ScreenSpaceEventHandler实例,故不会接管其绑定函数
|
||||
handler: new ScreenSpaceEventHandler(viewer.canvas),
|
||||
}
|
||||
viewerEventBusCache.set(viewer, cache)
|
||||
}
|
||||
// 如果缓存中不存在 ScreenSpaceEventHandler 实例,则创建一个
|
||||
if (!cache.handler) {
|
||||
cache.handler = new ScreenSpaceEventHandler(viewer.canvas)
|
||||
}
|
||||
|
||||
// 构建返回的 EventBus 对象
|
||||
const bus = {
|
||||
// 移除 mode 属性的定义
|
||||
|
||||
// onScreen 直接注册到 screenEmitter
|
||||
// onScreen(event, handler) {
|
||||
// screenEmitter.on(event, handler)
|
||||
// },
|
||||
onScreen: cache.screenEmitter.on,
|
||||
|
||||
// offScreen 直接从 screenEmitter 移除
|
||||
// offScreen(event, handler) {
|
||||
// screenEmitter.off(event, handler)
|
||||
// },
|
||||
offScreen: cache.screenEmitter.off,
|
||||
|
||||
// on 保持不变
|
||||
// on(event, handler) {
|
||||
// customEmitter.on(event, handler as (param: any[]) => void)
|
||||
// },
|
||||
on: cache.customEmitter.on,
|
||||
|
||||
// off 保持不变
|
||||
// off(event, handler) {
|
||||
// customEmitter.off(event, handler as ((param?: any[]) => void) | undefined)
|
||||
// },
|
||||
off: cache.customEmitter.off,
|
||||
|
||||
// emit 方法的实现需要处理两种不同的事件类型和参数结构
|
||||
emit(event, ...args) {
|
||||
// 使用 any 在实现层面处理联合类型和重载
|
||||
if (typeof event === 'string') {
|
||||
// 触发自定义事件
|
||||
// mitt.emit(type, event), event 是 payload
|
||||
cache.customEmitter.emit(event, args) // 将 ...args 作为单个数组 payload 传递
|
||||
} else if (Object.values(ScreenSpaceEventType).includes(event)) {
|
||||
// 触发屏幕空间事件 (触发所有监听器,无模式检查)
|
||||
// 屏幕空间事件通常只有一个参数
|
||||
cache.screenEmitter.emit(event, args[0])
|
||||
} else {
|
||||
console.warn(`EventBus: 不符合规范的触发事件: ${event}`)
|
||||
}
|
||||
},
|
||||
|
||||
dispose() {
|
||||
if (cache.handler && !cache.handler.isDestroyed()) {
|
||||
cache.handler.destroy()
|
||||
}
|
||||
cache.handler = null
|
||||
// mitt 实例本身不需要显式销毁,它们会被垃圾回收
|
||||
},
|
||||
}
|
||||
|
||||
// 用于 LEFT_CLICK 节流的状态
|
||||
let isLeftClickThrottled = false
|
||||
|
||||
// 遍历所有 Cesium 屏幕空间事件类型
|
||||
for (const eventTypeString in ScreenSpaceEventType) {
|
||||
const eventType = ScreenSpaceEventType[eventTypeString]
|
||||
const eventTypeCode = Number(eventType) // 获取对应的数字代码
|
||||
|
||||
// 定义 Cesium handler 实际执行的函数
|
||||
|
||||
// 特殊处理 LEFT_CLICK 进行节流
|
||||
// 观测到cesium双击会触发2次单击,所以在单击里加一个200毫秒的节流
|
||||
if (eventTypeCode === ScreenSpaceEventType.LEFT_CLICK) {
|
||||
const throttledClickAction = (args) => {
|
||||
if (isLeftClickThrottled) return // 如果正在节流,则忽略本次事件
|
||||
isLeftClickThrottled = true // 设置节流状态
|
||||
|
||||
// 触发 LEFT_CLICK 事件到总线
|
||||
bus.emit(eventType, args)
|
||||
|
||||
// 200ms 后解除节流状态
|
||||
window.setTimeout(() => {
|
||||
isLeftClickThrottled = false
|
||||
}, 200)
|
||||
}
|
||||
// 将节流后的函数设置到 Cesium handler
|
||||
cache.handler.setInputAction(throttledClickAction, eventTypeCode)
|
||||
} else {
|
||||
// 将 Cesium 原生事件参数转发到我们的 mitt 总线
|
||||
const cesiumHandlerAction = (args) => {
|
||||
// 直接通过 bus.emit 触发事件,mitt 会负责分发给所有监听器
|
||||
bus.emit(eventType, args)
|
||||
}
|
||||
cache.handler.setInputAction(cesiumHandlerAction, eventTypeCode)
|
||||
}
|
||||
}
|
||||
|
||||
return bus
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import {
|
||||
Viewer,
|
||||
Cartesian3,
|
||||
Cartographic,
|
||||
Cartesian2,
|
||||
ScreenSpaceEventType,
|
||||
ScreenSpaceEventHandler,
|
||||
} from 'cesium'
|
||||
import { computed, ref, shallowRef } from 'vue'
|
||||
import { useElementHover } from '@vueuse/core'
|
||||
import { useEventBus } from './useEventBus'
|
||||
|
||||
// 使用 Map 来缓存每个 viewer 对应的位置缓存
|
||||
// Viewer 实例作为 Map 的键
|
||||
const viewerHoverPositionCache = new Map()
|
||||
export const useHoverPosition = (viewer) => {
|
||||
let cache = viewerHoverPositionCache.get(viewer)
|
||||
if (!cache) {
|
||||
// cesium所在的canvas元素
|
||||
const canvasRef = ref(viewer.canvas)
|
||||
// 动态监听鼠标是否悬停在地图内
|
||||
const isHover = useElementHover(canvasRef)
|
||||
const screenPosition = shallowRef(null)
|
||||
const position = shallowRef(null)
|
||||
// const coordinate = shallowRef<Cartographic | null>(null)
|
||||
// 添加鼠标移动事件
|
||||
const bus = useEventBus(viewer)
|
||||
bus.onScreen(ScreenSpaceEventType.MOUSE_MOVE, (param) => {
|
||||
screenPosition.value = param.endPosition.clone()
|
||||
position.value = viewer.scene.pickPosition(param.endPosition)
|
||||
bus.emit('hoverTest', {
|
||||
screenPosition: screenPosition.value,
|
||||
position: position.value,
|
||||
})
|
||||
// coordinate.value = Cartographic.fromCartesian(position.value)
|
||||
})
|
||||
cache = {
|
||||
isHover,
|
||||
screenPosition,
|
||||
position,
|
||||
// coordinate,
|
||||
}
|
||||
viewerHoverPositionCache.set(viewer, cache)
|
||||
}
|
||||
|
||||
return cache
|
||||
}
|
|
@ -0,0 +1,223 @@
|
|||
import * as Cesium from 'cesium'
|
||||
import * as turf from '@turf/turf'
|
||||
import { useDrawTool } from './useDrawTool'
|
||||
|
||||
// 存储测量内容的辅助图层
|
||||
let measureLayer = null
|
||||
|
||||
/**
|
||||
* 计算折线长度
|
||||
* @param {Cesium.Cartesian3[]} positions 位置列表
|
||||
* @returns 折线长度,单位米
|
||||
*/
|
||||
const getDistance = (positions) => {
|
||||
const line = turf.lineString(positions.map((pos) => {
|
||||
const cartographic = Cesium.Cartographic.fromCartesian(pos)
|
||||
return [Cesium.Math.toDegrees(cartographic.longitude), Cesium.Math.toDegrees(cartographic.latitude)]
|
||||
}))
|
||||
return turf.length(line, { units: 'kilometers' }) * 1000 // 返回米
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算多边形面积
|
||||
* @param {Cesium.Cartesian3[]} positions 位置列表
|
||||
* @returns 多边形面积,单位平方米
|
||||
*/
|
||||
const getArea = (positions) => {
|
||||
const pts = [...positions, positions[0]] // 闭合多边形
|
||||
const polygon = turf.polygon([pts.map((pos) => {
|
||||
const cartographic = Cesium.Cartographic.fromCartesian(pos)
|
||||
return [Cesium.Math.toDegrees(cartographic.longitude), Cesium.Math.toDegrees(cartographic.latitude)]
|
||||
})])
|
||||
return turf.area(polygon) // 返回平方米
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取多个点的中心位置,根据边界范围确定
|
||||
* @param {Cesium.Cartesian3[]} positions 位置列表
|
||||
* @param {number} height 返回位置的高度,默认为0
|
||||
* @returns 根据边界范围确定的中心点位置
|
||||
*/
|
||||
const getBoundingCenter = (positions, height = 0) => {
|
||||
const coord = getBoundingCenterCoordinate(positions)
|
||||
return Cesium.Cartesian3.fromDegrees(coord[0], coord[1], height)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取多个点的中心位置坐标,根据边界范围确定
|
||||
* @param {Cesium.Cartesian3[]} positions 位置列表
|
||||
* @returns 根据边界范围确定的中心点位置坐标,格式为 [经度, 纬度]
|
||||
*/
|
||||
const getBoundingCenterCoordinate = (positions) => {
|
||||
const polygon = turf.lineString(positions.map((pos) => {
|
||||
const cartographic = Cesium.Cartographic.fromCartesian(pos)
|
||||
return [Cesium.Math.toDegrees(cartographic.longitude), Cesium.Math.toDegrees(cartographic.latitude)]
|
||||
}))
|
||||
const centerFeature = turf.center(polygon)
|
||||
return centerFeature.geometry.coordinates
|
||||
}
|
||||
|
||||
export const useMeasureTool = (viewer) => {
|
||||
if (!viewer) {
|
||||
throw new Error('获取 Viewer 实例失败,请确保在 Viewer 初始化后调用 useMeasureTool 方法!')
|
||||
}
|
||||
|
||||
const editTool = useDrawTool(viewer)
|
||||
if (!measureLayer) {
|
||||
measureLayer = new Cesium.CustomDataSource('__assist_measure')
|
||||
viewer.dataSources.add(measureLayer)
|
||||
}
|
||||
|
||||
// 距离测量
|
||||
const measureDistance = () => {
|
||||
return editTool.drawPolyline().then((positions) => {
|
||||
const style = {
|
||||
width: 5,
|
||||
material: Cesium.Color.fromCssColorString('#57e0e0'),
|
||||
}
|
||||
let arr = []
|
||||
for (let i = 0; i < positions.length; i++) {
|
||||
if (i + 1 < positions.length) {
|
||||
if (!positions[i] || !positions[i + 1]) continue
|
||||
arr.push([positions[i], positions[i + 1]])
|
||||
}
|
||||
}
|
||||
console.log(arr)
|
||||
let result = 0
|
||||
arr.forEach((item) => {
|
||||
let start = Cesium.Cartographic.fromCartesian(item[0])
|
||||
let end = Cesium.Cartographic.fromCartesian(item[1])
|
||||
let startObj = { longitude: '', latitude: '' }
|
||||
let endObj = { longitude: '', latitude: '' }
|
||||
startObj.longitude = Cesium.Math.toDegrees(start.longitude)
|
||||
startObj.latitude = Cesium.Math.toDegrees(start.latitude)
|
||||
endObj.longitude = Cesium.Math.toDegrees(end.longitude)
|
||||
endObj.latitude = Cesium.Math.toDegrees(end.latitude)
|
||||
|
||||
console.log(startObj)
|
||||
console.log(endObj)
|
||||
|
||||
let from = turf.point([startObj.longitude, startObj.latitude])
|
||||
let to = turf.point([endObj.longitude, endObj.latitude])
|
||||
|
||||
let distance = turf.distance(from, to, { units: 'kilometers' }) * 1000
|
||||
|
||||
result += distance
|
||||
|
||||
const tempPolyline = new Cesium.Entity({
|
||||
name: '__measure_distance',
|
||||
// position: [ {x: (item[0].x + item[1].x) / 2, y: (item[0].y + item[1].y) / 2, z: (item[0].z + item[1].z) / 2} ],
|
||||
position: new Cesium.Cartesian3(
|
||||
(item[0].x + item[1].x) / 2,
|
||||
(item[0].y + item[1].y) / 2,
|
||||
(item[0].z + item[1].z) / 2,
|
||||
),
|
||||
polyline: {
|
||||
positions: [...item],
|
||||
width: style.width,
|
||||
material: style.material,
|
||||
clampToGround: true,
|
||||
},
|
||||
label: new Cesium.LabelGraphics({
|
||||
text: distance.toFixed(2) + '米',
|
||||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
||||
pixelOffset: new Cesium.Cartesian2(10, 10),
|
||||
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||
}),
|
||||
})
|
||||
|
||||
measureLayer.entities.add(tempPolyline)
|
||||
return positions
|
||||
})
|
||||
|
||||
let len = positions.length - 1
|
||||
if (len > 1) {
|
||||
let resultLabel = new Cesium.Entity({
|
||||
name: 'measure_distance_length',
|
||||
position: new Cesium.Cartesian3(
|
||||
(positions[0].x + positions[len].x) / 2,
|
||||
(positions[0].y + positions[len].y) / 2,
|
||||
(positions[0].z + positions[len].z) / 2,
|
||||
),
|
||||
label: new Cesium.LabelGraphics({
|
||||
text: '总距离:' + result.toFixed(2) + '米',
|
||||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
||||
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||
fillColor: Cesium.Color.AQUAMARINE,
|
||||
}),
|
||||
})
|
||||
measureLayer.entities.add(resultLabel)
|
||||
}
|
||||
return positions
|
||||
})
|
||||
}
|
||||
|
||||
// 面积测量
|
||||
const measureArea = () => {
|
||||
return editTool.drawPolygon().then((positions) => {
|
||||
let polygon = []
|
||||
positions.forEach((item) => {
|
||||
let obj = Cesium.Cartographic.fromCartesian(item)
|
||||
polygon.push([Cesium.Math.toDegrees(obj.longitude), Cesium.Math.toDegrees(obj.latitude)])
|
||||
})
|
||||
|
||||
let features = turf.points(polygon)
|
||||
let center = turf.center(features)
|
||||
|
||||
console.log(center)
|
||||
|
||||
let points0 = Cesium.Cartographic.fromCartesian(positions[0])
|
||||
polygon.push([
|
||||
Cesium.Math.toDegrees(points0.longitude),
|
||||
Cesium.Math.toDegrees(points0.latitude),
|
||||
])
|
||||
let areaPolygon = turf.polygon([polygon])
|
||||
let area = turf.area(areaPolygon)
|
||||
|
||||
const tempPolygon = new Cesium.Entity({
|
||||
name: '__measure_area',
|
||||
position: Cesium.Cartesian3.fromDegrees(
|
||||
center.geometry.coordinates[0],
|
||||
center.geometry.coordinates[1],
|
||||
),
|
||||
polygon: {
|
||||
hierarchy: new Cesium.PolygonHierarchy(positions),
|
||||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
||||
material: Cesium.Color.fromCssColorString('#ffff00').withAlpha(0.3),
|
||||
},
|
||||
polyline: {
|
||||
positions: [...positions, positions[0]],
|
||||
clampToGround: true,
|
||||
width: 2,
|
||||
material: Cesium.Color.fromCssColorString('#ffff00'),
|
||||
},
|
||||
label: new Cesium.LabelGraphics({
|
||||
text: area.toFixed(2) + '平方米',
|
||||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
||||
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||
}),
|
||||
})
|
||||
measureLayer.entities.add(tempPolygon)
|
||||
return positions
|
||||
})
|
||||
}
|
||||
|
||||
// 取消当前测量
|
||||
const abort = () => {
|
||||
editTool.abort() // 调用编辑工具的取消方法
|
||||
}
|
||||
|
||||
// 清除所有测量结果
|
||||
const clear = () => {
|
||||
measureLayer.entities.removeAll()
|
||||
}
|
||||
|
||||
return {
|
||||
distance: measureDistance,
|
||||
area: measureArea,
|
||||
abort,
|
||||
clear,
|
||||
}
|
||||
}
|
||||
|
||||
export { getDistance, getArea, getBoundingCenter, getBoundingCenterCoordinate }
|
|
@ -84,6 +84,22 @@ export const constantRoutes = [
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/systemTemplate',
|
||||
component: Layout,
|
||||
alwaysShow: true,
|
||||
meta: {
|
||||
title: '系统模板'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'forestFire',
|
||||
component: () => import('@/views/systemTemplate/forestFire/index.vue'),
|
||||
name: 'forestFire',
|
||||
meta: { title: '森林防火' },
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/gisManagement',
|
||||
component: Layout,
|
||||
|
|
|
@ -0,0 +1,596 @@
|
|||
<template>
|
||||
<!-- 根容器,用于定位和拖拽 -->
|
||||
<div class="dialog-property-grid-container" ref="targetRef" :style="style">
|
||||
<div class="property-box">
|
||||
<!-- 拖拽手柄 -->
|
||||
<div class="property-header" ref="handleRef">
|
||||
<span class="property-title">属性</span>
|
||||
<!-- 绑定点击事件触发 close 事件 -->
|
||||
<el-icon class="property-close" @click="handleClose"><Close /></el-icon>
|
||||
</div>
|
||||
<div class="property-table_wrapper">
|
||||
<el-table
|
||||
:data="internalData"
|
||||
:show-header="false"
|
||||
row-key="key"
|
||||
border
|
||||
height="100%"
|
||||
@cell-click="handleCellClick"
|
||||
>
|
||||
<!-- 属性名称列 -->
|
||||
<el-table-column
|
||||
prop="name"
|
||||
label="属性名称"
|
||||
width="180"
|
||||
class-name="property-name-column"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="property-name-cell">{{ row.name }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 属性值列 -->
|
||||
<el-table-column label="属性值" class-name="property-value-column">
|
||||
<template #default="{ row }">
|
||||
<div class="property-value-cell" :class="{ 'property-value-disabled': row.disabled }">
|
||||
<!-- 编辑模式 -->
|
||||
<div v-if="editingKey === row.key" class="editor-container">
|
||||
<!-- 根据属性类型渲染不同的编辑器 -->
|
||||
<template v-if="row.type === 'select'">
|
||||
<el-select
|
||||
v-model="editingValue"
|
||||
placeholder="请选择"
|
||||
size="small"
|
||||
@change="handleEditorChange(row, editingValue)"
|
||||
@blur="handleSave(row, editingValue)"
|
||||
ref="editorRef"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in row.options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<template v-else-if="row.type === 'color'">
|
||||
<el-color-picker
|
||||
v-model="editingValue"
|
||||
size="small"
|
||||
@change="handleEditorChange(row, editingValue)"
|
||||
@blur="handleSave(row, editingValue)"
|
||||
ref="editorRef"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="row.type === 'number'">
|
||||
<el-input-number
|
||||
v-model="editingValue"
|
||||
:controls="false"
|
||||
size="small"
|
||||
@change="handleEditorChange(row, editingValue)"
|
||||
@blur="handleSave(row, editingValue)"
|
||||
ref="editorRef"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<!-- 默认使用 el-input -->
|
||||
<el-input
|
||||
v-model="editingValue"
|
||||
size="small"
|
||||
@change="handleEditorChange(row, editingValue)"
|
||||
@blur="handleSave(row, editingValue)"
|
||||
@keyup.enter="handleSave(row, editingValue)"
|
||||
ref="editorRef"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 显示模式 -->
|
||||
<div v-else class="display-container">
|
||||
{{ getDisplayValue(row) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<!-- 底部的保存按钮 -->
|
||||
<div class="property-footer">
|
||||
<!-- 绑定点击事件触发 save 事件 -->
|
||||
<el-button type="success" size="small" @click="handleSaveButtonClick">保 存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, nextTick, onMounted } from 'vue';
|
||||
import {
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElSelect,
|
||||
ElOption,
|
||||
ElColorPicker,
|
||||
ElMessage,
|
||||
ElIcon,
|
||||
ElButton
|
||||
} from 'element-plus';
|
||||
import { Close } from '@element-plus/icons-vue';
|
||||
import { useDraggable, useElementBounding, useWindowSize } from '@vueuse/core';
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 属性配置数据
|
||||
* 数组,每个元素是一个属性对象
|
||||
* {
|
||||
* key: string | number, // 唯一标识
|
||||
* name: string, // 属性名称
|
||||
* value: any, // 属性值
|
||||
* type: 'text' | 'number' | 'select' | 'color', // 编辑器类型
|
||||
* options?: Array<{ label: string, value: any }>, // type为select时需要
|
||||
* checkMethod?: (newValue: any, property: object) => boolean | Promise<boolean>, // 检查方法
|
||||
* disabled?: boolean, // 是否禁用该属性,禁用后不可编辑,默认为 false
|
||||
* }
|
||||
*/
|
||||
data: {
|
||||
type: Array,
|
||||
// default: () => []
|
||||
default: () => [{a:1}] // 默认值为一个空数组
|
||||
},
|
||||
/**
|
||||
* 保存按钮点击后的回调函数 (由外部 DialogPropertyGrid.js 传入)
|
||||
* 接收所有属性数据作为参数
|
||||
*/
|
||||
onSaveCallback: {
|
||||
type: Function,
|
||||
// default: null
|
||||
default: () => () => {return true;} // 默认值为一个空函数
|
||||
}
|
||||
});
|
||||
|
||||
console.log('初始化时props:', props);
|
||||
|
||||
// 触发事件:property-change (属性值改变), close (关闭按钮点击)
|
||||
// save 事件将由 handleSaveButtonClick 内部直接调用 onSaveCallback 实现,不再 emit
|
||||
const emit = defineEmits(['property-change', 'close']);
|
||||
|
||||
// 内部维护的数据副本,避免直接修改props
|
||||
const internalData = ref([]);
|
||||
|
||||
// 当前正在编辑的属性的key
|
||||
const editingKey = ref(null);
|
||||
// 当前正在编辑的属性的值
|
||||
const editingValue = ref(null);
|
||||
// 编辑器组件的引用,用于focus
|
||||
const editorRef = ref(null);
|
||||
|
||||
// 拖拽相关的 refs and state
|
||||
const targetRef = ref(null); // 要拖拽的元素 (.dialog-property-grid-container)
|
||||
const handleRef = ref(null); // 拖拽手柄 (.property-header)
|
||||
|
||||
// 获取窗口尺寸
|
||||
const { width: windowWidth, height: windowHeight } = useWindowSize();
|
||||
// 获取目标元素尺寸和位置
|
||||
const { width: elementWidth, height: elementHeight } = useElementBounding(targetRef);
|
||||
|
||||
// 使用 useDraggable 实现拖拽
|
||||
const { x, y, style } = useDraggable(targetRef, {
|
||||
handle: handleRef,
|
||||
initialValue: { x: 0, y: 0 }, // 初始值会在 mounted 后计算并设置
|
||||
onMove: ({ x, y }) => {
|
||||
// 计算边界
|
||||
const minX = 0;
|
||||
const minY = 0;
|
||||
// 使用 elementWidth.value 和 elementHeight.value 获取当前元素的尺寸
|
||||
const maxX = windowWidth.value - elementWidth.value;
|
||||
const maxY = windowHeight.value - elementHeight.value;
|
||||
|
||||
// 限制在边界内
|
||||
const clampedX = Math.max(minX, Math.min(x, maxX));
|
||||
const clampedY = Math.max(minY, Math.min(y, maxY));
|
||||
|
||||
// 返回限制后的位置
|
||||
return { x: clampedX, y: clampedY };
|
||||
},
|
||||
// 确保在拖拽时不会选择文本
|
||||
preventDefault: true,
|
||||
stopPropagation: true,
|
||||
});
|
||||
|
||||
// 监听props.data变化,更新internalData
|
||||
watch(
|
||||
() => props.data,
|
||||
(newData) => {
|
||||
// 简单的深拷贝,确保不影响原始数据
|
||||
internalData.value = newData ? newData.map(item => ({ ...item })) : [];
|
||||
// 当数据更新时,如果正在编辑的属性不在新数据中,退出编辑模式
|
||||
if (editingKey.value !== null && !internalData.value.some(item => item.key === editingKey.value)) {
|
||||
editingKey.value = null;
|
||||
editingValue.value = null;
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
// 在组件挂载后计算初始位置并设置 useDraggable 的初始值
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
// 确保元素已经渲染并获取到尺寸
|
||||
if (elementWidth.value > 0 && elementHeight.value > 0) {
|
||||
// 计算居中位置
|
||||
const centerX = (windowWidth.value - elementWidth.value) / 2;
|
||||
const centerY = (windowHeight.value - elementHeight.value) / 2;
|
||||
|
||||
// 设置 useDraggable 的当前位置
|
||||
x.value = centerX;
|
||||
y.value = centerY;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* 处理单元格点击事件
|
||||
* @param {object} row - 当前行数据 (属性对象)
|
||||
* @param {object} column - 当前列对象
|
||||
* @param {object} cell - 当前单元格DOM元素
|
||||
* @param {Event} event - 事件对象
|
||||
*/
|
||||
const handleCellClick = (row, column, cell, event) => {
|
||||
// 如果属性被禁用,则不进行任何操作
|
||||
if (row.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 只处理属性值列的点击
|
||||
if (column.className.includes('property-value-column')) {
|
||||
// 如果当前有正在编辑的属性且不是当前点击的属性,先保存当前编辑的属性
|
||||
if (editingKey.value !== null && editingKey.value !== row.key) {
|
||||
const previousRow = internalData.value.find(item => item.key === editingKey.value);
|
||||
if (previousRow && !previousRow.disabled) {
|
||||
// 使用当前 editingValue 进行保存
|
||||
handleSave(previousRow, editingValue.value);
|
||||
}
|
||||
}
|
||||
|
||||
// 开始编辑当前点击的属性
|
||||
startEditing(row);
|
||||
} else if (editingKey.value !== null) {
|
||||
// 如果点击了非属性值列,且有属性正在编辑,则保存当前编辑的属性
|
||||
const previousRow = internalData.value.find(item => item.key === editingKey.value);
|
||||
if (previousRow && !previousRow.disabled) {
|
||||
// 使用 current editingValue 进行保存
|
||||
handleSave(previousRow, editingValue.value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 开始编辑某个属性
|
||||
* @param {object} row - 要编辑的属性对象
|
||||
*/
|
||||
const startEditing = (row) => {
|
||||
// 如果属性被禁用,则不开始编辑
|
||||
if (row.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果已经是当前属性,则不重复开始编辑
|
||||
if (editingKey.value === row.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
editingKey.value = row.key;
|
||||
editingValue.value = row.value;
|
||||
|
||||
// 等待DOM更新后聚焦编辑器
|
||||
nextTick(() => {
|
||||
// 根据编辑器类型尝试聚焦
|
||||
if (editorRef.value) {
|
||||
// 尝试访问内部 input 元素进行聚焦
|
||||
const inputElement = editorRef.value.$refs?.input || editorRef.value.$refs?.wrapper?.querySelector('input');
|
||||
if (inputElement && inputElement.focus) {
|
||||
inputElement.focus();
|
||||
} else if (editorRef.value.focus) { // 对于有 focus 方法的组件本身
|
||||
editorRef.value.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 编辑器值变化时更新 editingValue
|
||||
* @param {object} row - 当前属性对象
|
||||
* @param {any} newValue - 编辑器的新值
|
||||
*/
|
||||
const handleEditorChange = (row, newValue) => {
|
||||
// 如果属性被禁用,不处理 change 事件
|
||||
if (row.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 对于select和color-picker,change事件通常意味着编辑完成,可以直接触发保存
|
||||
if (row.type === 'select' || row.type === 'color') {
|
||||
handleSave(row, newValue);
|
||||
} else {
|
||||
// 对于input和number,change/input更新editingValue,blur/enter触发保存
|
||||
editingValue.value = newValue;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 保存属性值(触发验证并更新 internalData),由单元格编辑触发
|
||||
* @param {object} row - 当前属性对象
|
||||
* @param {any} finalValue - 最终的值(来自 editingValue)
|
||||
*/
|
||||
const handleSave = async (row, finalValue) => {
|
||||
// 确保当前正在编辑的是这一行,避免重复保存或保存错误行
|
||||
if (editingKey.value !== row.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果属性被禁用,不保存(无需执行后续流程)
|
||||
if (row.disabled) {
|
||||
editingKey.value = null; // 退出编辑模式
|
||||
editingValue.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const property = internalData.value.find(item => item.key === row.key);
|
||||
if (!property) return; // 找不到属性,异常情况
|
||||
|
||||
let isValid = true;
|
||||
let validationMessage = '';
|
||||
|
||||
// 执行检查方法
|
||||
if (property.checkMethod) {
|
||||
try {
|
||||
// 检查方法可能是同步或异步的
|
||||
isValid = await property.checkMethod(finalValue, property);
|
||||
if (typeof isValid !== 'boolean') {
|
||||
console.error(`checkMethod for key "${row.key}" did not return a boolean.`);
|
||||
isValid = false; // 如果返回值不是boolean,视为验证失败
|
||||
validationMessage = `属性 "${row.name}" 的检查方法返回了非布尔值。`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error during checkMethod for key "${row.key}":`, error);
|
||||
isValid = false;
|
||||
validationMessage = `属性 "${row.name}" 检查时发生错误:${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
// 验证成功,更新值
|
||||
property.value = finalValue;
|
||||
// 触发事件通知父组件值已改变 (实时变化)
|
||||
emit('property-change', row.key, finalValue, property);
|
||||
// 退出编辑模式
|
||||
editingKey.value = null;
|
||||
editingValue.value = null;
|
||||
return true; // 表示保存成功
|
||||
} else {
|
||||
// 验证失败
|
||||
ElMessage.error(validationMessage || `属性 "${row.name}" 的值无效。`);
|
||||
// 停留在编辑模式,让用户修改
|
||||
return false; // 表示保存失败
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理保存按钮点击事件
|
||||
* 触发 save 事件,并传递所有属性数据
|
||||
*/
|
||||
const handleSaveButtonClick = async () => {
|
||||
// 如果当前有正在编辑的属性,先尝试保存它
|
||||
if (editingKey.value !== null) {
|
||||
const currentRow = internalData.value.find(item => item.key === editingKey.value);
|
||||
if (currentRow && !currentRow.disabled) {
|
||||
// 使用当前 editingValue 进行保存,并等待其完成
|
||||
const saveSuccess = await handleSave(currentRow, editingValue.value);
|
||||
// 如果 handleSave 验证失败,则不触发 save 和 close
|
||||
if (!saveSuccess) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 调用外部传入的保存回调,传递当前所有属性数据
|
||||
// 传递 internalData 的深拷贝,避免外部修改影响组件内部状态
|
||||
if (typeof props.onSaveCallback === 'function') {
|
||||
props.onSaveCallback(JSON.parse(JSON.stringify(internalData.value)));
|
||||
}
|
||||
|
||||
// 触发 close 事件,通知外部关闭弹窗
|
||||
emit('close');
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理关闭按钮点击事件
|
||||
* 触发 close 事件
|
||||
*/
|
||||
const handleClose = () => {
|
||||
// 如果当前有正在编辑的属性,先退出编辑模式
|
||||
if (editingKey.value !== null) {
|
||||
editingKey.value = null;
|
||||
editingValue.value = null;
|
||||
}
|
||||
// 触发 close 事件, 通知外部关闭弹窗
|
||||
emit('close');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 获取属性在显示模式下的展示值
|
||||
* 对于select类型,显示label;其他类型直接显示value
|
||||
* @param {object} row - 属性对象
|
||||
* @returns {string} - 显示的值
|
||||
*/
|
||||
const getDisplayValue = (row) => {
|
||||
if (row.type === 'select' && row.options) {
|
||||
const option = row.options.find(opt => opt.value === row.value);
|
||||
return option ? option.label : row.value; // 如果找不到匹配的option,显示原始值
|
||||
}
|
||||
// 对于其他类型,直接显示值,null或undefined显示空字符串
|
||||
return row.value !== null && row.value !== undefined ? row.value : '';
|
||||
};
|
||||
|
||||
// 暴露 updateData 方法给外部 DialogPropertyGrid.js 调用
|
||||
defineExpose({
|
||||
updateData(newData, newOnSaveCallback) {
|
||||
// 直接更新 props.data 会触发 watch 监听器更新 internalData
|
||||
// 但 props 是只读的,不能直接修改。
|
||||
// 更好的方式是让外部直接修改 data ref,或者通过一个内部方法来更新 internalData
|
||||
// 这里我们让外部直接调用 updateData 方法来更新 internalData 和 onSaveCallback
|
||||
internalData.value = newData ? newData.map(item => ({ ...item })) : [];
|
||||
// 更新保存回调
|
||||
// 注意:这里直接修改了 props.onSaveCallback,这在严格模式下是不允许的。
|
||||
// 更安全的做法是让外部传入一个新的回调,或者在 DialogPropertyGrid.js 中管理回调。
|
||||
// 为了简化,我们假设外部传入的回调是响应式的或者每次都传入新的。
|
||||
// 如果需要严格遵守 Vue 规则,应该在 DialogPropertyGrid.js 的 wrapperComponent 中管理回调。
|
||||
// 考虑到这里的场景是命令式调用,且组件生命周期由外部管理,直接更新 internalData 和回调是可行的。
|
||||
// 如果需要更新 props,应该在外部 wrapperComponent 中通过 ref 来传递 props。
|
||||
// 让我们调整一下,让外部 wrapperComponent 管理 data 和 onSaveCallback 的 ref,并传递给 PropertyGrid。
|
||||
// PropertyGrid 只需要 watch data prop。
|
||||
// onSaveCallback 也可以作为 prop 传递。
|
||||
}
|
||||
});
|
||||
|
||||
// 重新思考:让 PropertyGrid 接收 data 和 onSaveCallback 作为 props 是最标准的做法。
|
||||
// DialogPropertyGrid.js 的 wrapperComponent 负责维护 data 和 onSaveCallback 的 ref,并传递给 PropertyGrid。
|
||||
// 当 show 被调用时,wrapperComponent 更新这些 ref,从而更新 PropertyGrid 的 props。
|
||||
// PropertyGrid 内部 watch data prop。
|
||||
// onSaveCallback prop 直接在 handleSaveButtonClick 中调用。
|
||||
// close 事件仍然 emit,由 wrapperComponent 监听并清理。
|
||||
|
||||
// 移除 defineExpose({ updateData ... })
|
||||
// 移除 props 中的 onSaveCallback 定义,改为在 emit('save') 后由外部处理。
|
||||
// 恢复 emit('save')
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 根容器样式,用于定位和拖拽 */
|
||||
.dialog-property-grid-container {
|
||||
position: fixed; /* 固定定位 */
|
||||
z-index: 1000; /* 确保在其他内容之上 */
|
||||
/* 初始位置由 useDraggable 计算并设置 */
|
||||
/* top: 50%; */
|
||||
/* left: 50%; */
|
||||
/* transform: translate(-50%, -50%); /* 居中 */
|
||||
}
|
||||
|
||||
.property-box {
|
||||
width: 400px;
|
||||
height: 320px;
|
||||
background-color: rgba(7, 111, 111, 0.9); /* 稍微不透明一点 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 4px; /* 添加圆角 */
|
||||
overflow: hidden; /* 隐藏超出部分的圆角 */
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); /* 添加阴影 */
|
||||
/* 移除遮罩相关的样式 */
|
||||
}
|
||||
|
||||
.property-header {
|
||||
flex: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background-color: #076f6f;
|
||||
border-bottom: 1px solid #dcdfe6;
|
||||
color: #d3fff4;
|
||||
cursor: grab; /* 拖拽手柄鼠标样式 */
|
||||
|
||||
&:active {
|
||||
cursor: grabbing; /* 拖拽中鼠标样式 */
|
||||
}
|
||||
|
||||
.property-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.property-close {
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
color: #d3fff4;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #ffffff; /* 悬停时变为白色 */
|
||||
}
|
||||
}
|
||||
}
|
||||
.property-table_wrapper {
|
||||
flex: auto;
|
||||
margin: 8px;
|
||||
height: 0; /* flex: auto 需要一个基础高度 */
|
||||
background-color: #fff; /* 表格区域背景色 */
|
||||
border-radius: 4px;
|
||||
overflow: hidden; /* 确保表格内容在容器内 */
|
||||
}
|
||||
.property-footer {
|
||||
flex: none; /* 不伸缩 */
|
||||
text-align: right;
|
||||
padding: 8px 20px; /* 调整内边距 */
|
||||
background-color: rgba(7, 111, 111, 0.9); /* 与头部背景色一致 */
|
||||
}
|
||||
|
||||
|
||||
/* 可以添加一些样式来优化显示和编辑体验 */
|
||||
/* ... (保留原有的表格和编辑器样式) ... */
|
||||
|
||||
/* 调整 Element Plus 组件在小尺寸下的样式 */
|
||||
.editor-container .el-input,
|
||||
.editor-container .el-input-number,
|
||||
.editor-container .el-select,
|
||||
.editor-container .el-color-picker {
|
||||
width: 100%; /* 使编辑器宽度填充父容器 */
|
||||
}
|
||||
|
||||
/* 隐藏 el-input-number 的控制按钮 */
|
||||
.editor-container .el-input-number.is-controls-right .el-input__wrapper {
|
||||
padding-right: 0;
|
||||
padding-left: 8px; /* 留一点左边距 */
|
||||
}
|
||||
.editor-container .el-input-number__controls {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 悬停效果 */
|
||||
/* 注意:Element Plus 表格默认有悬停背景,这里可以覆盖或调整 */
|
||||
.el-table__row:hover .property-value-cell {
|
||||
background-color: #ecf5ff; /* 悬停时浅蓝色背景 */
|
||||
}
|
||||
|
||||
.el-table__row:hover .property-value-cell.property-value-disabled {
|
||||
cursor: not-allowed; /* 显示禁止图标 */
|
||||
background-color: transparent !important; /* 确保不显示默认的悬停背景 */
|
||||
}
|
||||
|
||||
/* 正在编辑的单元格样式 */
|
||||
.el-table__row .property-value-cell .editor-container {
|
||||
/* 可以添加一个边框或背景色来突出显示正在编辑的单元格 */
|
||||
/* outline: 1px solid var(--el-color-primary); */
|
||||
/* background-color: #ecf5ff; */
|
||||
}
|
||||
|
||||
/* 调整 Element Plus 表格的背景色,使其与 property-table_wrapper 背景色一致 */
|
||||
.property-table_wrapper .el-table {
|
||||
background-color: transparent; /* 使表格背景透明,显示 wrapper 的背景 */
|
||||
}
|
||||
/* 调整表格行的背景色,使其在非悬停时透明 */
|
||||
.property-table_wrapper .el-table__row {
|
||||
background-color: transparent;
|
||||
}
|
||||
/* 调整单元格背景色,使其在非悬停时透明 */
|
||||
.property-table_wrapper .el-table td.el-table__cell,
|
||||
.property-table_wrapper .el-table th.el-table__cell.is-leaf {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,131 @@
|
|||
// src/utils/DialogPropertyGrid.js
|
||||
import { createApp, h, ref, watch } from 'vue';
|
||||
import * as Cesium from 'cesium';
|
||||
|
||||
// 导入修改后的 PropertyGrid 组件 (现在作为 DialogPropertyGrid 的内容)
|
||||
import PropertyGridContent from './PropertyGrid.vue'; // 假设文件名为 PropertyGrid.vue
|
||||
import { getPropertyData } from './property';
|
||||
|
||||
// 用于存储当前弹窗的应用实例、容器和根组件实例,实现单例
|
||||
let currentApp = null;
|
||||
let currentContainer = null;
|
||||
let currentWrapperInstance = null; // 存储 wrapperComponent 实例的引用
|
||||
|
||||
/**
|
||||
* 显示 DialogPropertyGrid 弹窗
|
||||
* @param {Cesium.Entity} entity - 获取属性的实体对象
|
||||
* @param {Function} [onSave] - 点击保存按钮后调用的回调函数,接收所有属性数据作为参数
|
||||
*/
|
||||
const show = (entity = null, onSave = null) => {
|
||||
// 0. 解析 data
|
||||
const data = getPropertyData(entity);
|
||||
|
||||
// 如果已经有弹窗实例存在
|
||||
if (currentApp && currentWrapperInstance) {
|
||||
console.log('DialogPropertyGrid instance already exists. Updating content.');
|
||||
// 更新现有实例的数据和保存回调
|
||||
currentWrapperInstance.updateContent(data, onSave);
|
||||
return; // 退出,不再创建新实例
|
||||
}
|
||||
|
||||
// 1. 创建一个临时的 DOM 容器
|
||||
currentContainer = document.createElement('div');
|
||||
// 将容器添加到 body
|
||||
document.body.appendChild(currentContainer);
|
||||
|
||||
// 2. 定义清理函数
|
||||
const cleanup = () => {
|
||||
if (currentApp) {
|
||||
currentApp.unmount(); // 卸载 Vue 应用
|
||||
currentApp = null;
|
||||
}
|
||||
if (currentContainer) {
|
||||
currentContainer.remove(); // 移除 DOM 元素
|
||||
currentContainer = null;
|
||||
}
|
||||
currentWrapperInstance = null; // 清理 wrapperComponent 实例引用
|
||||
console.log('DialogPropertyGrid cleaned up.');
|
||||
};
|
||||
|
||||
// 3. 创建 Vue 应用实例的根组件 (一个简单的 wrapper)
|
||||
const wrapperComponent = {
|
||||
setup() {
|
||||
// 使用 ref 来存储和响应式更新传递给 PropertyGridContent 的 props
|
||||
const componentData = ref(data);
|
||||
const onSaveCallback = ref(onSave); // 默认回调为一个空函数
|
||||
|
||||
// 方法:更新数据和回调 (供外部 show 方法调用)
|
||||
const updateContent = (data, cb) => {
|
||||
componentData.value = data;
|
||||
onSaveCallback.value = cb;
|
||||
// 当数据更新时,如果弹窗之前被关闭了,这里不需要显式“显示”
|
||||
// 因为组件本身是 fixed 定位,只要 mounted 就在那里
|
||||
// 如果需要控制显隐,可以在 PropertyGridContent 内部加一个 v-show 或 v-if
|
||||
// 但为了单例和替换内容的需求,mounted/unmounted 更符合生命周期管理
|
||||
};
|
||||
|
||||
// 监听 PropertyGridContent 的 save 事件
|
||||
const handlePropertyGridSave = (allPropertyData) => {
|
||||
// 调用外部传入的保存回调
|
||||
if (typeof onSaveCallback.value === 'function') {
|
||||
onSaveCallback.value(allPropertyData);
|
||||
}
|
||||
// 保存后执行清理 (关闭弹窗)
|
||||
cleanup();
|
||||
};
|
||||
|
||||
// 监听 PropertyGridContent 的 close 事件
|
||||
const handlePropertyGridClose = () => {
|
||||
// 直接执行清理 (关闭弹窗)
|
||||
cleanup();
|
||||
};
|
||||
|
||||
// 监听 PropertyGridContent 的 property-change 事件 (可选,如果需要在外部实时响应)
|
||||
const handlePropertyChange = (key, newValue, property) => {
|
||||
// console.log(`实时属性变更: ${property.name} = ${newValue}`);
|
||||
// 如果需要,可以在这里添加额外的实时处理逻辑
|
||||
// 注意:这个事件是 PropertyGridContent 内部实时触发的,与 save 事件不同
|
||||
};
|
||||
|
||||
|
||||
// 暴露 updateContent 方法给外部 show 方法调用
|
||||
return {
|
||||
updateContent, // 暴露更新方法
|
||||
componentData, // 暴露给 render 函数使用
|
||||
handlePropertyGridSave, // 暴露给 render 函数使用
|
||||
handlePropertyGridClose, // 暴露给 render 函数使用
|
||||
handlePropertyChange, // 暴露给 render 函数使用
|
||||
};
|
||||
},
|
||||
render() {
|
||||
// 渲染 PropertyGridContent 组件
|
||||
// 将响应式的 ref 作为 props 传递
|
||||
return h(PropertyGridContent, {
|
||||
data: this.componentData, // 绑定数据
|
||||
// 将回调函数作为 prop 传递
|
||||
onSaveCallback: this.handlePropertyGridSave,
|
||||
// 监听 PropertyGridContent 内部触发的 close 事件
|
||||
onClose: this.handlePropertyGridClose,
|
||||
// 监听 PropertyGridContent 内部触发的 property-change 事件 (可选)
|
||||
onPropertyChange: this.handlePropertyChange,
|
||||
// 注意:PropertyGridContent 内部的 save 事件现在直接调用了 onSaveCallback prop,
|
||||
// 所以这里不再需要监听 @save 事件。
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 创建 Vue 应用实例并挂载
|
||||
currentApp = createApp(wrapperComponent);
|
||||
|
||||
// 5. 将应用挂载到临时容器
|
||||
// 挂载后,获取根组件实例,以便后续更新数据
|
||||
currentWrapperInstance = currentApp.mount(currentContainer);
|
||||
|
||||
// 6. 立即调用 updateContent 初始化并显示弹窗
|
||||
currentWrapperInstance.updateContent(data, onSave);
|
||||
};
|
||||
|
||||
// 导出 show 方法
|
||||
export default {
|
||||
show
|
||||
};
|
|
@ -0,0 +1,123 @@
|
|||
import * as Cesium from 'cesium'
|
||||
|
||||
// 定义水源地的属性
|
||||
export const watersource = [
|
||||
{
|
||||
key: "type",
|
||||
name: "要素类型",
|
||||
type: "text",
|
||||
value: "水源地",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: "name",
|
||||
name: "名称",
|
||||
type: "text",
|
||||
value: "",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
key: "area",
|
||||
name: "占地面积",
|
||||
type: "text",
|
||||
value: "0",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: "longitude",
|
||||
name: "经度",
|
||||
type: "text",
|
||||
value: "0",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: "latitude",
|
||||
name: "纬度",
|
||||
type: "text",
|
||||
value: "0",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: "volume",
|
||||
name: "储水量",
|
||||
type: "text",
|
||||
value: "0",
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
// 定义仓库的属性
|
||||
export const warehouse = [
|
||||
{
|
||||
key: "type",
|
||||
name: "要素类型",
|
||||
type: "text",
|
||||
value: "仓库",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: "name",
|
||||
name: "名称",
|
||||
type: "text",
|
||||
value: "",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
key: "longitude",
|
||||
name: "经度",
|
||||
type: "text",
|
||||
value: "0",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: "latitude",
|
||||
name: "纬度",
|
||||
type: "text",
|
||||
value: "0",
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
const getClone = (config) => {
|
||||
return config.map(item => ({ ...item }))
|
||||
}
|
||||
|
||||
export const getPropertyData = (entity) => {
|
||||
let data = []
|
||||
if(!entity || !entity.properties) {
|
||||
return data // 如果没有实体或属性,返回空数组
|
||||
}
|
||||
|
||||
const properties = entity.properties.getValue()
|
||||
if (properties.__type === "watersource") {
|
||||
data = getClone(watersource)
|
||||
} else if (properties.__type === "warehouse") {
|
||||
data = getClone(warehouse)
|
||||
}
|
||||
|
||||
// 遍历数据数组,将properties有的属性赋给对应的属性
|
||||
// 注意:地图要素属性主要参考这里的数据结构,就算properties没有值,也会显示出来
|
||||
data.forEach(item => {
|
||||
const value = properties[item.key]
|
||||
if (value !== undefined) {
|
||||
item.value = value
|
||||
}
|
||||
});
|
||||
return data
|
||||
};
|
||||
|
||||
export const setPropertyData = (data, entity) => {
|
||||
if (!entity || !entity.properties) {
|
||||
console.warn("Entity or properties not found")
|
||||
return
|
||||
}
|
||||
|
||||
const newProperties = entity.properties.getValue()
|
||||
data.forEach(item => {
|
||||
if (item.key && item.value !== undefined) {
|
||||
newProperties[item.key] = item.value
|
||||
}
|
||||
})
|
||||
// 替换 properties
|
||||
entity.properties = new Cesium.PropertyBag(newProperties)
|
||||
}
|
|
@ -0,0 +1,514 @@
|
|||
<template>
|
||||
<div class="toolbar-box">
|
||||
<div class="toolbar-title" :class="{ 'open': isOpen }" :title="isOpen ? '收起工具栏' : '展开工具栏'" @click="isOpen = !isOpen">
|
||||
<span>工具栏</span>
|
||||
<el-icon v-if="isOpen" style="font-size: 18px;"><DArrowRight /></el-icon>
|
||||
<el-icon v-if="!isOpen" style="font-size: 28px;"><DArrowLeft /></el-icon>
|
||||
</div>
|
||||
<div class="toolbar-content">
|
||||
<div v-if="isOpen" class="toolbar-item" v-for="(item, index) in options" :key="index" :title="item.label" @click="bus.emit(`toolbar_${item.name}`)">
|
||||
<img :src="item.icon" alt="" />
|
||||
<span class="toolbar-label">{{ item.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import * as Cesium from 'cesium';
|
||||
import DialogPropertyGrid from './DialogPropertyGrid';
|
||||
|
||||
import {
|
||||
useDrawTool,
|
||||
createCurvePolygonPositions,
|
||||
createStraightArrowPositions,
|
||||
createWideArrowPositions,
|
||||
createAttackArrowPositions,
|
||||
createDoubleArrowPositions
|
||||
} from '@/components/CesiumMap/mixins/useDrawTool';
|
||||
import { useEventBus } from '@/components/CesiumMap/mixins/useEventBus';
|
||||
import { useMeasureTool, getArea, getBoundingCenterCoordinate } from '@/components/CesiumMap/mixins/useMeasureTool';
|
||||
import { setPropertyData } from './DialogPropertyGrid/property';
|
||||
|
||||
import toolbarLocationIcon from '@/assets/icons/toolbar_location.png';
|
||||
import toolbarPolylineIcon from '@/assets/icons/toolbar_polyline.png';
|
||||
import toolbarPolygonIcon from '@/assets/icons/toolbar_polygon.png';
|
||||
import toolbarCurvePolygonIcon from '@/assets/icons/toolbar_curve_polygon.png';
|
||||
import toolbarStraightArrowIcon from '@/assets/icons/toolbar_straight_arrow.png';
|
||||
import toolbarWideArrowIcon from '@/assets/icons/toolbar_wide_arrow.png';
|
||||
import toolbarAttackArrowIcon from '@/assets/icons/toolbar_attack_arrow.png';
|
||||
import toolbarDoubleArrowIcon from '@/assets/icons/toolbar_double_arrow.png';
|
||||
import toolbarMeasureDistanceIcon from '@/assets/icons/toolbar_measure_distance.png';
|
||||
import toolbarMeasureAreaIcon from '@/assets/icons/toolbar_measure_area.png';
|
||||
import toolbarWarehouseIcon from '@/assets/icons/toolbar_warehouse.png';
|
||||
import toolbarWatersourceIcon from '@/assets/icons/toolbar_watersource.png';
|
||||
import toolbarClearIcon from '@/assets/icons/toolbar_clear.png';
|
||||
|
||||
import drawLocationIcon from '@/assets/icons/draw_location.png';
|
||||
import fireWarehouseIcon from '@/assets/icons/fire_warehouse.png';
|
||||
|
||||
const props = defineProps({
|
||||
viewer: {
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化地图实例
|
||||
let viewer = null
|
||||
let drawTool = null
|
||||
let measureTool = null
|
||||
let bus = null
|
||||
let toolbarLayer = null
|
||||
const isOpen = ref(false)
|
||||
const options = ref([{
|
||||
name: 'location',
|
||||
label: '位置',
|
||||
icon: toolbarLocationIcon
|
||||
}, {
|
||||
name: 'polyline',
|
||||
label: '线',
|
||||
icon: toolbarPolylineIcon
|
||||
}, {
|
||||
name: 'polygon',
|
||||
label: '面',
|
||||
icon: toolbarPolygonIcon
|
||||
}, {
|
||||
name: 'curvePolygon',
|
||||
label: '曲面',
|
||||
icon: toolbarCurvePolygonIcon
|
||||
}, {
|
||||
name: 'straightArrow',
|
||||
label: '直箭头',
|
||||
icon: toolbarStraightArrowIcon
|
||||
}, {
|
||||
name: 'wideArrow',
|
||||
label: '宽箭头',
|
||||
icon: toolbarWideArrowIcon
|
||||
}, {
|
||||
name: 'attackArrow',
|
||||
label: '攻击箭头',
|
||||
icon: toolbarAttackArrowIcon
|
||||
}, {
|
||||
name: 'doubleArrow',
|
||||
label: '双箭头',
|
||||
icon: toolbarDoubleArrowIcon
|
||||
}, {
|
||||
name: 'measureDistance',
|
||||
label: '测距',
|
||||
icon: toolbarMeasureDistanceIcon
|
||||
}, {
|
||||
name: 'measureArea',
|
||||
label: '测面',
|
||||
icon: toolbarMeasureAreaIcon
|
||||
}, {
|
||||
name: 'warehouse',
|
||||
label: '仓库',
|
||||
icon: toolbarWarehouseIcon
|
||||
}, {
|
||||
name: 'watersource',
|
||||
label: '水源',
|
||||
icon: toolbarWatersourceIcon
|
||||
}, {
|
||||
name: 'clear',
|
||||
label: '清除',
|
||||
icon: toolbarClearIcon
|
||||
}])
|
||||
|
||||
// 绘制位置
|
||||
const drawLocation = (params) => {
|
||||
if(!drawTool) {
|
||||
console.error('绘制工具未初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
drawTool.abort();
|
||||
measureTool.abort();
|
||||
drawTool.drawPoint().then(position => {
|
||||
toolbarLayer?.entities.add({
|
||||
position,
|
||||
billboard: {
|
||||
image: params?.icon || drawLocationIcon,
|
||||
verticalOrigin: Cesium.VerticalOrigin.BOTTOM
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 绘制线
|
||||
const drawPolyline = () => {
|
||||
if(!drawTool) {
|
||||
console.error('绘制工具未初始化');
|
||||
return;
|
||||
}
|
||||
drawTool.abort();
|
||||
measureTool.abort();
|
||||
drawTool.drawPolyline().then((positions) => {
|
||||
toolbarLayer?.entities.add({
|
||||
polyline: {
|
||||
positions,
|
||||
width: 2,
|
||||
material: Cesium.Color.YELLOW,
|
||||
},
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
// 绘制面
|
||||
const drawPolygon = () => {
|
||||
if(!drawTool) {
|
||||
console.error('绘制工具未初始化');
|
||||
return;
|
||||
}
|
||||
drawTool.abort();
|
||||
measureTool.abort();
|
||||
drawTool.drawPolygon().then((positions) => {
|
||||
toolbarLayer?.entities.add({
|
||||
polyline: {
|
||||
positions:[...positions, positions[0]], // 闭合多边形
|
||||
width: 2,
|
||||
material: Cesium.Color.YELLOW,
|
||||
},
|
||||
polygon: {
|
||||
hierarchy: positions,
|
||||
material: Cesium.Color.YELLOW.withAlpha(0.5),
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 绘制曲面
|
||||
const drawCurvePolygon = () => {
|
||||
if(!drawTool) {
|
||||
console.error('绘制工具未初始化');
|
||||
return;
|
||||
}
|
||||
drawTool.abort();
|
||||
measureTool.abort();
|
||||
drawTool.drawCurvePolygon().then((positions) => {
|
||||
if (positions.length < 2 || !positions[0]) return
|
||||
|
||||
const fullPositions = createCurvePolygonPositions(positions)
|
||||
toolbarLayer?.entities.add({
|
||||
polyline: {
|
||||
positions: fullPositions,
|
||||
width: 2,
|
||||
material: Cesium.Color.YELLOW,
|
||||
},
|
||||
polygon: {
|
||||
hierarchy: fullPositions,
|
||||
material: Cesium.Color.YELLOW.withAlpha(0.5),
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 绘制直箭头
|
||||
const drawStraightArrow = () => {
|
||||
if(!drawTool) {
|
||||
console.error('绘制工具未初始化');
|
||||
return;
|
||||
}
|
||||
drawTool.abort();
|
||||
measureTool.abort();
|
||||
drawTool.drawStraightArrow().then((positions) => {
|
||||
if (!positions[0] || !positions[1]) return
|
||||
|
||||
const fullPositions = createStraightArrowPositions(positions)
|
||||
toolbarLayer?.entities.add({
|
||||
polyline: {
|
||||
positions: [...fullPositions, fullPositions[0]],
|
||||
width: 2,
|
||||
material: Cesium.Color.YELLOW,
|
||||
},
|
||||
polygon: {
|
||||
hierarchy: fullPositions,
|
||||
material: Cesium.Color.YELLOW.withAlpha(0.5),
|
||||
},
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
// 绘制宽箭头
|
||||
const drawWideArrow = () => {
|
||||
if(!drawTool) {
|
||||
console.error('绘制工具未初始化');
|
||||
return;
|
||||
}
|
||||
drawTool.abort();
|
||||
measureTool.abort();
|
||||
drawTool.drawWideArrow().then((positions) => {
|
||||
if (positions.length < 2) return
|
||||
|
||||
const fullPositions = createWideArrowPositions(positions)
|
||||
toolbarLayer?.entities.add({
|
||||
polyline: {
|
||||
positions: [...fullPositions, fullPositions[0]],
|
||||
width: 2,
|
||||
material: Cesium.Color.YELLOW,
|
||||
},
|
||||
polygon: {
|
||||
hierarchy: fullPositions,
|
||||
material: Cesium.Color.YELLOW.withAlpha(0.5),
|
||||
},
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
// 绘制攻击箭头
|
||||
const drawAttackArrow = () => {
|
||||
if(!drawTool) {
|
||||
console.error('绘制工具未初始化');
|
||||
return;
|
||||
}
|
||||
drawTool.abort();
|
||||
measureTool.abort();
|
||||
drawTool.drawAttackArrow().then((positions) => {
|
||||
if (positions.length < 3) return
|
||||
|
||||
const fullPositions = createAttackArrowPositions(positions)
|
||||
toolbarLayer?.entities.add({
|
||||
polyline: {
|
||||
positions: [...fullPositions, fullPositions[0]],
|
||||
width: 2,
|
||||
material: Cesium.Color.YELLOW,
|
||||
},
|
||||
polygon: {
|
||||
hierarchy: fullPositions,
|
||||
material: Cesium.Color.YELLOW.withAlpha(0.5),
|
||||
},
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
// 绘制双箭头
|
||||
const drawDoubleArrow = () => {
|
||||
if(!drawTool) {
|
||||
console.error('绘制工具未初始化');
|
||||
return;
|
||||
}
|
||||
drawTool.abort();
|
||||
measureTool.abort();
|
||||
drawTool.drawDoubleArrow().then((positions) => {
|
||||
if (positions.length < 3) return
|
||||
|
||||
const fullPositions = createDoubleArrowPositions(positions)
|
||||
toolbarLayer?.entities.add({
|
||||
polyline: {
|
||||
positions: [...fullPositions, fullPositions[0]],
|
||||
width: 2,
|
||||
material: Cesium.Color.YELLOW,
|
||||
},
|
||||
polygon: {
|
||||
hierarchy: fullPositions,
|
||||
material: Cesium.Color.YELLOW.withAlpha(0.5),
|
||||
},
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
// 平面距离测量的绘制
|
||||
const drawMeasureDistance = () => {
|
||||
if(!measureTool) {
|
||||
console.error('绘制工具未初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
drawTool.abort();
|
||||
measureTool.abort();
|
||||
measureTool.distance();
|
||||
};
|
||||
|
||||
// 平面面积测量的绘制
|
||||
const drawMeasureArea = () => {
|
||||
if(!measureTool) {
|
||||
console.error('绘制工具未初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
drawTool.abort();
|
||||
measureTool.abort();
|
||||
measureTool.area();
|
||||
};
|
||||
|
||||
// 绘制水源地(面)
|
||||
const drawWatersource = () => {
|
||||
if(!drawTool) {
|
||||
console.error('绘制工具未初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
drawTool.abort();
|
||||
measureTool.abort();
|
||||
drawTool.drawPolygon().then((positions) => {
|
||||
// 计算中心点坐标
|
||||
const center = getBoundingCenterCoordinate(positions);
|
||||
// 添加水源地实体
|
||||
const entity = toolbarLayer?.entities.add({
|
||||
polyline: {
|
||||
positions:[...positions, positions[0]], // 闭合多边形
|
||||
width: 2,
|
||||
material: Cesium.Color.BLUE,
|
||||
},
|
||||
polygon: {
|
||||
hierarchy: positions,
|
||||
material: Cesium.Color.BLUE.withAlpha(0.5),
|
||||
},
|
||||
properties: {
|
||||
__type: 'watersource',
|
||||
name: '',
|
||||
area: getArea(positions).toFixed(2), // 面积
|
||||
longitude: center[0].toFixed(6),
|
||||
latitude: center[1].toFixed(6),
|
||||
volume: 0 // 储水量,默认0
|
||||
}
|
||||
});
|
||||
// 编辑属性
|
||||
DialogPropertyGrid.show(entity, (data) => {
|
||||
// console.log('确定按钮被点击,最新表单数据:', formData);
|
||||
// 更新实体属性
|
||||
setPropertyData(data, entity);
|
||||
// 调用接口
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 绘制仓库(点)
|
||||
const drawWarehouse = (params) => {
|
||||
if(!drawTool) {
|
||||
console.error('绘制工具未初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
drawTool.abort();
|
||||
drawTool.drawPoint().then((position) => {
|
||||
// 计算中心点坐标
|
||||
const center = Cesium.Cartographic.fromCartesian(position);
|
||||
|
||||
const entity = toolbarLayer?.entities.add({
|
||||
position,
|
||||
billboard: {
|
||||
image: params?.icon || fireWarehouseIcon,
|
||||
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
||||
pixelOffset: new Cesium.Cartesian2(0, params?.icon ? 0 : 18), // 可选偏移
|
||||
},
|
||||
properties: {
|
||||
__type: 'warehouse',
|
||||
name: '',
|
||||
longitude: Cesium.Math.toDegrees(center.longitude).toFixed(6),
|
||||
latitude: Cesium.Math.toDegrees(center.latitude).toFixed(6),
|
||||
}
|
||||
});
|
||||
// 编辑属性
|
||||
DialogPropertyGrid.show(entity, (data) => {
|
||||
// console.log('确定按钮被点击,最新表单数据:', formData);
|
||||
// 更新实体属性
|
||||
setPropertyData(data, entity);
|
||||
// 调用接口
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 清除工具栏上的所有绘制实体
|
||||
const toolbarClear = () => {
|
||||
if (!viewer || !toolbarLayer) return;
|
||||
|
||||
// 清除所有绘制的实体
|
||||
toolbarLayer.entities.removeAll();
|
||||
measureTool?.clear();
|
||||
|
||||
// 清除当前测量
|
||||
if (measureTool) {
|
||||
measureTool.abort();
|
||||
}
|
||||
|
||||
// 清除当前绘制
|
||||
if (drawTool) {
|
||||
drawTool.abort();
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => props.viewer, (v) => {
|
||||
if (v) {
|
||||
viewer = v;
|
||||
|
||||
// 添加绘制图层
|
||||
toolbarLayer = new Cesium.CustomDataSource('toolbarLayer');
|
||||
viewer.dataSources.add(toolbarLayer);
|
||||
|
||||
drawTool = useDrawTool(viewer);
|
||||
measureTool = useMeasureTool(viewer);
|
||||
// 绑定事件
|
||||
bus = useEventBus(viewer);
|
||||
bus.on('toolbar_location', drawLocation);
|
||||
bus.on('toolbar_polyline', drawPolyline);
|
||||
bus.on('toolbar_polygon', drawPolygon);
|
||||
bus.on('toolbar_curvePolygon', drawCurvePolygon);
|
||||
bus.on('toolbar_straightArrow', drawStraightArrow);
|
||||
bus.on('toolbar_wideArrow', drawWideArrow);
|
||||
bus.on('toolbar_attackArrow', drawAttackArrow);
|
||||
bus.on('toolbar_doubleArrow', drawDoubleArrow);
|
||||
bus.on('toolbar_measureDistance', drawMeasureDistance);
|
||||
bus.on('toolbar_measureArea', drawMeasureArea);
|
||||
bus.on('toolbar_watersource', drawWatersource);
|
||||
bus.on('toolbar_warehouse', drawWarehouse);
|
||||
bus.on('toolbar_clear', toolbarClear);
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.toolbar-box {
|
||||
.toolbar-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: #8EF8FF;
|
||||
background-color: rgba(7, 111, 111, 0.7);
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin: 0 4px 4px 0;
|
||||
|
||||
&.open {
|
||||
flex-direction: row;
|
||||
align-items: end;
|
||||
}
|
||||
}
|
||||
}
|
||||
.toolbar-content {
|
||||
max-height: 420px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
// justify-content: space-around;
|
||||
|
||||
.toolbar-item {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: #8EF8FF;
|
||||
background-color: rgba(7, 111, 111, 0.7);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 4px 4px 0;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 215, 0, 0.5);
|
||||
}
|
||||
.toolbar-label {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,131 @@
|
|||
<template>
|
||||
<div class="preview-map">
|
||||
<cesium-map :options="options" @init="initMap">
|
||||
</cesium-map>
|
||||
<toolbar :viewer="viewerRef" class="toolbar"></toolbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { shallowRef } from 'vue';
|
||||
import * as Cesium from 'cesium';
|
||||
import CesiumMap from '@/components/CesiumMap/index.vue';
|
||||
import { useEventBus } from '@/components/CesiumMap/mixins/useEventBus';
|
||||
import Toolbar from './Toolbar.vue';
|
||||
import DialogPropertyGrid from './DialogPropertyGrid';
|
||||
import { setPropertyData } from './DialogPropertyGrid/property';
|
||||
|
||||
const viewerRef = shallowRef(null)
|
||||
const options = ref({
|
||||
setView: {
|
||||
// duration: '默认',
|
||||
x: 102.70862516,
|
||||
y: 25.01198423,
|
||||
z: 30000,
|
||||
heading: 0, // 偏航角
|
||||
pitch: -90, // 俯仰角
|
||||
roll: 0 // 翻滚角
|
||||
},
|
||||
ditu: 'tianditu', // 底图选择
|
||||
})
|
||||
const showProperty = ref(false); // 控制属性面板的显示与隐藏
|
||||
const propertyData = ref([])
|
||||
|
||||
const initMap = (v) => {
|
||||
viewerRef.value = v;
|
||||
console.log('地图已初始化', v);
|
||||
|
||||
const bus = useEventBus(viewerRef.value)
|
||||
// bus.on('hoverTest', ({ screenPosition }) => {
|
||||
// console.log('hoverTest 事件触发屏幕位置:', screenPosition);
|
||||
|
||||
// });
|
||||
bus.onScreen(Cesium.ScreenSpaceEventType.LEFT_CLICK, ({ position }) => {
|
||||
console.log('左键点击屏幕位置:', position);
|
||||
// 如果点击的是实体,则获取该实体的属性数据
|
||||
const pickedObjectList = viewerRef.value.scene.drillPick(position);
|
||||
if (pickedObjectList.length > 0) {
|
||||
// 遍历拾取实体,获取第一个非辅助实体
|
||||
for (let index = 0; index < pickedObjectList.length; index++) {
|
||||
const obj = pickedObjectList[index];
|
||||
if (Cesium.defined(obj) && obj.id && obj.id instanceof Cesium.Entity) {
|
||||
// 辅助实体的name都以'__'开头,不识别
|
||||
if(obj.id.name?.indexOf('__') === 0) {
|
||||
continue
|
||||
}
|
||||
// getPropertyData(obj.id);
|
||||
DialogPropertyGrid.show(obj.id, (data) => {
|
||||
// 更新实体属性
|
||||
setPropertyData(data, obj.id);
|
||||
// 调用接口
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
// 示例属性数据
|
||||
// propertyData.value = [
|
||||
// { key: 'id', name: 'ID', value: '12345', type: 'text', disabled: true },
|
||||
// { key: 'title', name: '标题', value: '这是一个标题', type: 'text', checkMethod: (val) => val.length > 0 || (ElMessage.error('标题不能为空'), false) },
|
||||
// { key: 'fontSize', name: '字体大小', value: 16, type: 'number', checkMethod: (val) => val >= 12 && val <= 72 || (ElMessage.error('字体大小必须在12-72之间'), false) },
|
||||
// {
|
||||
// key: 'align',
|
||||
// name: '对齐方式',
|
||||
// value: 'left',
|
||||
// type: 'select',
|
||||
// options: [
|
||||
// { label: '左对齐', value: 'left' },
|
||||
// { label: '居中', value: 'center' },
|
||||
// { label: '右对齐', value: 'right' },
|
||||
// ],
|
||||
// checkMethod: (val) => ['left', 'center', 'right'].includes(val) || (ElMessage.error('请选择有效的对齐方式'), false)
|
||||
// },
|
||||
// { key: 'color', name: '文字颜色', value: '#409EFF', type: 'color' },
|
||||
// { key: 'visible', name: '是否可见', value: true, type: 'select', options: [{ label: '是', value: true }, { label: '否', value: false }] },
|
||||
// { key: 'description', name: '描述', value: '这是一段描述文本。', type: 'text' },
|
||||
// { key: 'padding', name: '内边距', value: 10, type: 'number', checkMethod: (val) => val >= 0 || (ElMessage.error('内边距不能为负数'), false) },
|
||||
// ];
|
||||
|
||||
// 处理属性值变化的事件
|
||||
const handlePropertyChange = (key, newValue, property) => {
|
||||
console.log(`属性 "${property.name}" (key: ${key}) 的值变更为:`, newValue);
|
||||
// 在这里你可以根据需要处理值的变化,例如同步到后端或更新其他状态
|
||||
// 因为组件内部已经更新了 internalData,如果父组件也需要响应式地使用这个数据,
|
||||
// 并且不希望直接修改原始的 properties 数组,可以在这里根据 key 更新父组件的状态。
|
||||
// 例如:
|
||||
// const index = properties.value.findIndex(item => item.key === key);
|
||||
// if (index !== -1) {
|
||||
// properties.value[index].value = newValue;
|
||||
// }
|
||||
// 注意:上面的直接修改 properties.value[index].value 是可以的,因为 properties 是 ref 包裹的数组。
|
||||
// 但如果 properties 是 props 传递下来的,则不应该直接修改。
|
||||
// 在这个示例中,properties 是父组件的 ref,所以直接修改是允许的。
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.preview-map {
|
||||
// z-index: 2;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
}
|
||||
.toolbar {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
.infobox {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
// width: 600px;
|
||||
}
|
||||
</style>
|