diff --git a/package.json b/package.json index cf061db..6f643bc 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@element-plus/icons-vue": "2.3.1", + "@turf/turf": "^7.2.0", "@vueup/vue-quill": "1.2.0", "@vueuse/core": "10.11.0", "axios": "0.28.1", @@ -26,6 +27,7 @@ "js-cookie": "3.0.5", "jsencrypt": "3.3.2", "json-editor-vue": "^0.18.1", + "mitt": "^3.0.1", "nprogress": "0.2.0", "pinia": "2.1.7", "splitpanes": "3.1.5", diff --git a/src/assets/icons/draw_location.png b/src/assets/icons/draw_location.png new file mode 100644 index 0000000..312d146 Binary files /dev/null and b/src/assets/icons/draw_location.png differ diff --git a/src/assets/icons/fire_warehouse.png b/src/assets/icons/fire_warehouse.png new file mode 100644 index 0000000..6f716cc Binary files /dev/null and b/src/assets/icons/fire_warehouse.png differ diff --git a/src/assets/icons/toolbar_attack_arrow.png b/src/assets/icons/toolbar_attack_arrow.png new file mode 100644 index 0000000..e78f04f Binary files /dev/null and b/src/assets/icons/toolbar_attack_arrow.png differ diff --git a/src/assets/icons/toolbar_clear.png b/src/assets/icons/toolbar_clear.png new file mode 100644 index 0000000..cf3a66b Binary files /dev/null and b/src/assets/icons/toolbar_clear.png differ diff --git a/src/assets/icons/toolbar_curve_polygon.png b/src/assets/icons/toolbar_curve_polygon.png new file mode 100644 index 0000000..a1e4794 Binary files /dev/null and b/src/assets/icons/toolbar_curve_polygon.png differ diff --git a/src/assets/icons/toolbar_double_arrow.png b/src/assets/icons/toolbar_double_arrow.png new file mode 100644 index 0000000..9080626 Binary files /dev/null and b/src/assets/icons/toolbar_double_arrow.png differ diff --git a/src/assets/icons/toolbar_location.png b/src/assets/icons/toolbar_location.png new file mode 100644 index 0000000..303e88c Binary files /dev/null and b/src/assets/icons/toolbar_location.png differ diff --git a/src/assets/icons/toolbar_measure_area.png b/src/assets/icons/toolbar_measure_area.png new file mode 100644 index 0000000..749c8c3 Binary files /dev/null and b/src/assets/icons/toolbar_measure_area.png differ diff --git a/src/assets/icons/toolbar_measure_distance.png b/src/assets/icons/toolbar_measure_distance.png new file mode 100644 index 0000000..afab6c2 Binary files /dev/null and b/src/assets/icons/toolbar_measure_distance.png differ diff --git a/src/assets/icons/toolbar_polygon.png b/src/assets/icons/toolbar_polygon.png new file mode 100644 index 0000000..9a1cf32 Binary files /dev/null and b/src/assets/icons/toolbar_polygon.png differ diff --git a/src/assets/icons/toolbar_polyline.png b/src/assets/icons/toolbar_polyline.png new file mode 100644 index 0000000..5adc35b Binary files /dev/null and b/src/assets/icons/toolbar_polyline.png differ diff --git a/src/assets/icons/toolbar_straight_arrow.png b/src/assets/icons/toolbar_straight_arrow.png new file mode 100644 index 0000000..cddd6e6 Binary files /dev/null and b/src/assets/icons/toolbar_straight_arrow.png differ diff --git a/src/assets/icons/toolbar_warehouse.png b/src/assets/icons/toolbar_warehouse.png new file mode 100644 index 0000000..3a0f2b1 Binary files /dev/null and b/src/assets/icons/toolbar_warehouse.png differ diff --git a/src/assets/icons/toolbar_watersource.png b/src/assets/icons/toolbar_watersource.png new file mode 100644 index 0000000..af329d1 Binary files /dev/null and b/src/assets/icons/toolbar_watersource.png differ diff --git a/src/assets/icons/toolbar_wide_arrow.png b/src/assets/icons/toolbar_wide_arrow.png new file mode 100644 index 0000000..0a04086 Binary files /dev/null and b/src/assets/icons/toolbar_wide_arrow.png differ diff --git a/src/components/CesiumMap/mixins/useConfigSetting.js b/src/components/CesiumMap/mixins/useConfigSetting.js index b12cb5c..31830d2 100644 --- a/src/components/CesiumMap/mixins/useConfigSetting.js +++ b/src/components/CesiumMap/mixins/useConfigSetting.js @@ -63,6 +63,10 @@ export function useConfigSetting(viewer) { // 地图设置视角 const mapSetView = (options) => { + if (!options.setView) { + return; + } + let destination = Cesium.Cartesian3.fromDegrees(options.setView.x, options.setView.y, options.setView.z); let orientation = { heading: Cesium.Math.toRadians(options.setView.heading), diff --git a/src/components/CesiumMap/mixins/useDrawTool/draw/attackArrowGraphic.js b/src/components/CesiumMap/mixins/useDrawTool/draw/attackArrowGraphic.js new file mode 100644 index 0000000..c5470d4 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/draw/attackArrowGraphic.js @@ -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]))) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/draw/attackStraightArrowGraphic.js b/src/components/CesiumMap/mixins/useDrawTool/draw/attackStraightArrowGraphic.js new file mode 100644 index 0000000..42c58b2 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/draw/attackStraightArrowGraphic.js @@ -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]))) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/draw/curvePolygonGraphic.js b/src/components/CesiumMap/mixins/useDrawTool/draw/curvePolygonGraphic.js new file mode 100644 index 0000000..a00f2a5 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/draw/curvePolygonGraphic.js @@ -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]))) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/draw/doubleArrowGraphic.js b/src/components/CesiumMap/mixins/useDrawTool/draw/doubleArrowGraphic.js new file mode 100644 index 0000000..cfa5c25 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/draw/doubleArrowGraphic.js @@ -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 +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/draw/drawUtil.js b/src/components/CesiumMap/mixins/useDrawTool/draw/drawUtil.js new file mode 100644 index 0000000..776901e --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/draw/drawUtil.js @@ -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 diff --git a/src/components/CesiumMap/mixins/useDrawTool/draw/straightArrowGraphic.js b/src/components/CesiumMap/mixins/useDrawTool/draw/straightArrowGraphic.js new file mode 100644 index 0000000..af2ee5e --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/draw/straightArrowGraphic.js @@ -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]))) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/draw/wideArrowGraphic.js b/src/components/CesiumMap/mixins/useDrawTool/draw/wideArrowGraphic.js new file mode 100644 index 0000000..adaaf40 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/draw/wideArrowGraphic.js @@ -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]))) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/drawAttackArrow.js b/src/components/CesiumMap/mixins/useDrawTool/drawAttackArrow.js new file mode 100644 index 0000000..1a8cc8a --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/drawAttackArrow.js @@ -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) + }) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/drawCurvePolygon.js b/src/components/CesiumMap/mixins/useDrawTool/drawCurvePolygon.js new file mode 100644 index 0000000..4304024 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/drawCurvePolygon.js @@ -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) + }) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/drawDoubleArrow.js b/src/components/CesiumMap/mixins/useDrawTool/drawDoubleArrow.js new file mode 100644 index 0000000..8a5a607 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/drawDoubleArrow.js @@ -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) + }) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/drawPoint.js b/src/components/CesiumMap/mixins/useDrawTool/drawPoint.js new file mode 100644 index 0000000..2ce7848 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/drawPoint.js @@ -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) + }) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/drawPolygon.js b/src/components/CesiumMap/mixins/useDrawTool/drawPolygon.js new file mode 100644 index 0000000..25eebe9 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/drawPolygon.js @@ -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) + }) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/drawPolyline.js b/src/components/CesiumMap/mixins/useDrawTool/drawPolyline.js new file mode 100644 index 0000000..936beb4 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/drawPolyline.js @@ -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) + }) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/drawStraightArrow.js b/src/components/CesiumMap/mixins/useDrawTool/drawStraightArrow.js new file mode 100644 index 0000000..cbfcc78 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/drawStraightArrow.js @@ -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) + }) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/drawWideArrow.js b/src/components/CesiumMap/mixins/useDrawTool/drawWideArrow.js new file mode 100644 index 0000000..8afef2d --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/drawWideArrow.js @@ -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) + }) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/index.js b/src/components/CesiumMap/mixins/useDrawTool/index.js new file mode 100644 index 0000000..c5fc9e7 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/index.js @@ -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 } diff --git a/src/components/CesiumMap/mixins/useEventBus.js b/src/components/CesiumMap/mixins/useEventBus.js new file mode 100644 index 0000000..247ab7c --- /dev/null +++ b/src/components/CesiumMap/mixins/useEventBus.js @@ -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 +} diff --git a/src/components/CesiumMap/mixins/useHoverPosition.js b/src/components/CesiumMap/mixins/useHoverPosition.js new file mode 100644 index 0000000..e3d70a7 --- /dev/null +++ b/src/components/CesiumMap/mixins/useHoverPosition.js @@ -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(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 +} diff --git a/src/components/CesiumMap/mixins/useMeasureTool.js b/src/components/CesiumMap/mixins/useMeasureTool.js new file mode 100644 index 0000000..0ce60d6 --- /dev/null +++ b/src/components/CesiumMap/mixins/useMeasureTool.js @@ -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 } diff --git a/src/router/index.js b/src/router/index.js index 37a167b..a5ce564 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -84,6 +84,22 @@ export const constantRoutes = [ } ] }, + { + path: '/systemTemplate', + component: Layout, + alwaysShow: true, + meta: { + title: '系统模板' + }, + children: [ + { + path: 'forestFire', + component: () => import('@/views/systemTemplate/forestFire/index.vue'), + name: 'forestFire', + meta: { title: '森林防火' }, + }, + ] + }, { path: '/gisManagement', component: Layout, diff --git a/src/views/systemTemplate/forestFire/DialogPropertyGrid/PropertyGrid.vue b/src/views/systemTemplate/forestFire/DialogPropertyGrid/PropertyGrid.vue new file mode 100644 index 0000000..4cffc9c --- /dev/null +++ b/src/views/systemTemplate/forestFire/DialogPropertyGrid/PropertyGrid.vue @@ -0,0 +1,596 @@ + + + + + diff --git a/src/views/systemTemplate/forestFire/DialogPropertyGrid/index.js b/src/views/systemTemplate/forestFire/DialogPropertyGrid/index.js new file mode 100644 index 0000000..47b30e0 --- /dev/null +++ b/src/views/systemTemplate/forestFire/DialogPropertyGrid/index.js @@ -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 +}; diff --git a/src/views/systemTemplate/forestFire/DialogPropertyGrid/property.js b/src/views/systemTemplate/forestFire/DialogPropertyGrid/property.js new file mode 100644 index 0000000..9bdd852 --- /dev/null +++ b/src/views/systemTemplate/forestFire/DialogPropertyGrid/property.js @@ -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) +} diff --git a/src/views/systemTemplate/forestFire/Toolbar.vue b/src/views/systemTemplate/forestFire/Toolbar.vue new file mode 100644 index 0000000..b03ed56 --- /dev/null +++ b/src/views/systemTemplate/forestFire/Toolbar.vue @@ -0,0 +1,514 @@ + + + + + \ No newline at end of file diff --git a/src/views/systemTemplate/forestFire/index.vue b/src/views/systemTemplate/forestFire/index.vue new file mode 100644 index 0000000..756597c --- /dev/null +++ b/src/views/systemTemplate/forestFire/index.vue @@ -0,0 +1,131 @@ + + + + + \ No newline at end of file