Cesium实现鼠标拖动、移动效果

12 阅读1分钟

主要功能

点击广告牌的时候,可以在固定的高度上拖动,没有使用entity是考虑效率问题,主要实现思路是在当前点的垂直方向创建一个平面,移动的时候是获取鼠标位置对应的射线,从而获取焦点,直接上代码

/**
 * 广告牌,左键点击可以移动
 */
function generateDroneSvg(text: string): string {
	return `
<svg xmlns='http://www.w3.org/2000/svg' 
     xmlns:xlink='http://www.w3.org/1999/xlink' 
     viewBox='0 0 42 35' width='42' height='35'>
    <defs>
        <filter id='a' width='150.8%' height='171.7%' x='-25.4%' y='-34.2%' filterUnits='objectBoundingBox'>
            <feMorphology in='SourceAlpha' operator='dilate' radius='1' result='shadowSpreadOuter1'/>
            <feOffset in='shadowSpreadOuter1' result='shadowOffsetOuter1'/>
            <feGaussianBlur in='shadowOffsetOuter1' result='shadowBlurOuter1' stdDeviation='2'/>
            <feComposite in='shadowBlurOuter1' in2='SourceAlpha' operator='out' result='shadowBlurOuter1'/>
            <feColorMatrix in='shadowBlurOuter1' values='0 0 0 0 0.039215686 0 0 0 0 0.933333333 0 0 0 0 0.545098039 0 0 0 1 0'/>
        </filter>
        <path id='b' d='M16.832 3.248l14.132 21.197A1 1 0 0130.13 26H1.87a1 1 0 01-.833-1.555L15.168 3.248a1 1 0 011.664 0z'/>
    </defs>
    <g fill='none' fill-rule='evenodd'>
        <g transform='matrix(1 0 0 -1 5 32)'>
            <use fill='#000' filter='url(#a)' xlink:href='#b'/>
            <path fill='#00D690' stroke='#FFF' stroke-linejoin='round' stroke-width='2' d='M15.955 3.871L30.13 25H1.87L15.955 3.871z'/>
        </g>
        <text fill='#FFF' font-size='14' font-weight='500'>
            <tspan x='50%' y='50%' dy='.25em' text-anchor='middle'>${text}</tspan>
        </text>
    </g>
</svg>`;
}
// 2. SVG 转 Data URL 工具函数
function svgToDataUrl(svgStr: string): string {
	// 使用 encodeURIComponent 编码,兼容性好且无需处理 base64 编码问题
	const encoded = encodeURIComponent(svgStr).replace(/'/g, '%27').replace(/"/g, '%22');

	return `data:image/svg+xml;charset=utf-8,${encoded}`;
}
interface XYZ {
	x: number;
	y: number;
	z: number;
}
import {
	Viewer,
	Billboard,
	BillboardCollection,
	Cartesian3,
	ScreenSpaceEventHandler,
	ScreenSpaceEventType,
	defined,
	Cartesian2,
	IntersectionTests,
	Plane,
	Cartographic,
	Math as CesiumMath,
} from 'cesium';

export class DroneBillboard {
	viewer: Viewer;
	billboards: BillboardCollection;
	listsMap: Map<string, Billboard> = new Map();
	handler: ScreenSpaceEventHandler;
	getPositionFn!: (id: string, e: XYZ) => void;
	private isDragging: boolean = false;
	private draggedBillboard: Billboard | null = null;
	private dragPlane: Plane | undefined;
	private dragId: string = '';
	constructor(viewer: Viewer) {
		this.viewer = viewer;
		this.billboards = this.viewer.scene.primitives.add(new BillboardCollection());
		this.handler = new ScreenSpaceEventHandler(this.viewer.scene.canvas);
		this.bindEvents();
	}

	addFn(myId: string, text: string, x = 0, y = 0, z = 0) {
		const imageUrl = svgToDataUrl(generateDroneSvg(text));

		if (this.listsMap.has(myId)) {
			const item = this.listsMap.get(myId);

			if (item) {
				item.position = Cartesian3.fromDegrees(x, y, z);
				item.image = imageUrl;
			}
		} else {
			const item = this.billboards.add({
				position: Cartesian3.fromDegrees(x, y, z),
				image: imageUrl,
			});

			this.listsMap.set(myId, item);
		}
	}

	removeFn(myId: string) {
		const item = this.listsMap.get(myId);

		if (item) {
			this.billboards.remove(item);
			this.listsMap.delete(myId);
		}
	}

	private bindEvents() {
		const scene = this.viewer.scene;
		const canvas = scene.canvas;
		const ellipsoid = scene.globe.ellipsoid;

		this.handler.setInputAction((movement: { position: Cartesian2 }) => {
			const pickedObject = scene.pick(movement.position);
			if (defined(pickedObject) && defined(pickedObject.primitive)) {
				for (const [id, billboard] of this.listsMap) {
					if (pickedObject.primitive === billboard && billboard.position) {
						this.isDragging = true;
						this.dragId = id;
						this.draggedBillboard = billboard;
						canvas.style.cursor = 'grabbing';
						this.viewer.scene.screenSpaceCameraController.enableInputs = false;
						// 2. 创建一次平面!
						// 法线是当前点的地垂线方向
						const normal = ellipsoid.geodeticSurfaceNormal(billboard.position, new Cartesian3());

						this.dragPlane = Plane.fromPointNormal(billboard.position, normal);
						break;
					}
				}
			}
		}, ScreenSpaceEventType.LEFT_DOWN);

		this.handler.setInputAction((movement: { startPosition: Cartesian2; endPosition: Cartesian2 }) => {
			if (!this.isDragging || !this.draggedBillboard) {
				return;
			}
			// 1. 获取鼠标位置对应的射线
			const ray = scene.camera.getPickRay(movement.endPosition);
			if (defined(ray) && this.dragPlane) {
				const intersection = IntersectionTests.rayPlane(ray, this.dragPlane);
				if (defined(intersection)) {
					this.draggedBillboard.position = intersection;
					//回调
					const cartographic = Cartographic.fromCartesian(intersection);
					const { longitude, latitude, height } = cartographic;
					this.getPositionFn(this.dragId, {
						x: CesiumMath.toDegrees(longitude),
						y: CesiumMath.toDegrees(latitude),
						z: height,
					});
				}
			}
		}, ScreenSpaceEventType.MOUSE_MOVE);

		this.handler.setInputAction(() => {
			if (this.isDragging) {
				//回调
				// if ( this.draggedBillboard && this.draggedBillboard.position){
				//   const cartographic = Cartographic.fromCartesian( this.draggedBillboard.position);
				//   const { longitude, latitude, height } = cartographic;
				//   this.getPositionFn(this.dragId, {
				//     x: CesiumMath.toDegrees(longitude),
				//     y: CesiumMath.toDegrees(latitude),
				//     z: height
				//   });
				// }
				this.isDragging = false;
				this.dragId = '';
				this.draggedBillboard = null;
				canvas.style.cursor = 'default';
				this.viewer.scene.screenSpaceCameraController.enableInputs = true;
			}
		}, ScreenSpaceEventType.LEFT_UP);
	}
	getPosition(fn: (id: string, e: XYZ) => void) {
		this.getPositionFn = fn;
	}
	destroy() {
		if (this.viewer) {
			if (this.handler) {
				this.handler.destroy();
				this.handler = null!;
			}
			if (this.billboards) {
				this.viewer.scene.primitives.remove(this.billboards);
				this.billboards = null!;
			}
			this.listsMap.clear();
			this.getPositionFn = null!;
			this.draggedBillboard = null;
			this.dragPlane = undefined;
		}
	}
}

调用方法

import { DroneBillboard } from '/@/components/Cesium/DroneBillboardClass';
let droneBillboards: DroneBillboard | null = null;
const billboardFn = () => {
	if (viewer) {
		droneBillboards = new DroneBillboard(viewer);

		for (let i = 0; i < polygon.length; i++) {
			droneBillboards.addFn('billboard-' + i, (i + 1).toString(), polygon[i].lng, polygon[i].lat, 10);
		}
		droneBillboards.getPosition((id, p) => {
			if (id) {
				const index = id.split('-')[1];
				if (index !== undefined) {
					const item = polygon[Number(index)];
					item.lng = p.x;
					item.lat = p.y;
					polylineFn();
					// polygonFn();
				}
			}
		});
	}
};