1.添加森林防火地图模板页的初版;2.添加工具栏,集成绘制位置、线、面、曲面、直箭头、宽箭头、攻击箭头、双箭头、测距、测面功能;3.添加清除功能,删除所有绘制、测量的地图要素;4.添加仓库、水源地的绘制;5.实现属性添加、编辑机制;6.添加弹窗,可以显示和修改当前新增、点击的地图要素属性;
|
@ -13,6 +13,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "2.3.1",
|
"@element-plus/icons-vue": "2.3.1",
|
||||||
|
"@turf/turf": "^7.2.0",
|
||||||
"@vueup/vue-quill": "1.2.0",
|
"@vueup/vue-quill": "1.2.0",
|
||||||
"@vueuse/core": "10.11.0",
|
"@vueuse/core": "10.11.0",
|
||||||
"axios": "0.28.1",
|
"axios": "0.28.1",
|
||||||
|
@ -26,6 +27,7 @@
|
||||||
"js-cookie": "3.0.5",
|
"js-cookie": "3.0.5",
|
||||||
"jsencrypt": "3.3.2",
|
"jsencrypt": "3.3.2",
|
||||||
"json-editor-vue": "^0.18.1",
|
"json-editor-vue": "^0.18.1",
|
||||||
|
"mitt": "^3.0.1",
|
||||||
"nprogress": "0.2.0",
|
"nprogress": "0.2.0",
|
||||||
"pinia": "2.1.7",
|
"pinia": "2.1.7",
|
||||||
"splitpanes": "3.1.5",
|
"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) => {
|
const mapSetView = (options) => {
|
||||||
|
if (!options.setView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let destination = Cesium.Cartesian3.fromDegrees(options.setView.x, options.setView.y, options.setView.z);
|
let destination = Cesium.Cartesian3.fromDegrees(options.setView.x, options.setView.y, options.setView.z);
|
||||||
let orientation = {
|
let orientation = {
|
||||||
heading: Cesium.Math.toRadians(options.setView.heading),
|
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',
|
path: '/gisManagement',
|
||||||
component: Layout,
|
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>
|