1.添加森林防火地图模板页的初版;2.添加工具栏,集成绘制位置、线、面、曲面、直箭头、宽箭头、攻击箭头、双箭头、测距、测面功能;3.添加清除功能,删除所有绘制、测量的地图要素;4.添加仓库、水源地的绘制;5.实现属性添加、编辑机制;6.添加弹窗,可以显示和修改当前新增、点击的地图要素属性;

This commit is contained in:
zhangquan 2025-07-11 17:17:49 +08:00
parent 21403a5643
commit ec4ca333b2
42 changed files with 4219 additions and 0 deletions

View File

@ -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",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1010 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -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),

View File

@ -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])))
}

View File

@ -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])))
}

View File

@ -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])))
}

View File

@ -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
}

View File

@ -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

View File

@ -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])))
}

View File

@ -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])))
}

View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -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 }

View File

@ -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
}

View File

@ -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
}

View File

@ -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 }

View File

@ -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,

View File

@ -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 }>, // typeselect
* 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.datainternalData
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;
}
// selectcolor-pickerchange
if (row.type === 'select' || row.type === 'color') {
handleSave(row, newValue);
} else {
// inputnumberchange/inputeditingValueblur/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
}
// nullundefined
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>

View File

@ -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
};

View File

@ -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)
}

View File

@ -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>

View File

@ -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>