cesium自定义指南针

370 阅读4分钟

一、项目介绍

项目需要一个指南针,用鼠标按住旋转指针,会让3d地图旋转,点击中心点重置方位。当然可以使用插件cesium-navigation-es6,但是样式不好修改,所以我自定义了一个指南针。

image.png

实现思路

在Cesium中,相机的方向由 headingpitch 和 roll 三个角度参数控制,它们共同定义了相机在3D空间中的朝向。要让地图旋转,只要控制相机的角度即可。

heading(偏航角),绕(z)轴旋转,类似于指南针的方向,范围0 到  弧度(或  到 360°)。

  • 0 或  表示正北方向(默认Cesium的初始朝向)。
  • π/2(90°)表示正东。
  • π(180°)表示正南。
  • 3π/2(270°)表示正西。

Pitch(俯仰角),绕(y)轴旋转,控制相机的“抬头”或“低头”。 范围-π/2 到 π/2,超过会导致视角翻转。

  • 0 表示相机水平朝前(平行于地面)。

  • π/2(90°)表示相机垂直向下(俯视地面)。

  • -π/2(-90°)表示相机垂直向上(仰视天空)。

Roll(翻滚角),绕(X)轴旋转的角度(倾斜左右)。在飞行模拟中模拟飞机倾斜,但Cesium中较少直接使用(默认通常为 0

所以我们要先计算鼠标点击指南针时向左向右偏移的角度,将这个角度值转为相机旋转的弧度值,并重新设置给相机。

指南针实现

首先先创建一个div和css样式画出一个指南针:

 <div class="compass-container">
    <div
      class="compass-outer-ring"
      ref="compassOuterRing"
      @click="handleClick"
      @mousedown="onMouseDown"
      @mouseup="onMouseUp"
      @mousemove="onMouseMove"
    >
      <div class="compass-inner-ring">
        <div class="compass-arrow">
          <div class="compass-arrow-icon"></div>
          <div class="compass-arrow-icon-bottom"></div>
          <div class="compass-arrow-north">N</div>
          <div class="compass-arrow-south">S</div>
          <div class="compass-arrow-west">w</div>
          <div class="compass-arrow-east">E</div>
        </div>
      </div>
    </div>
  </div>

指南针的css样式如下:

/* 指北针样式 */
.compass-container {
  position: relative;
  width: 40px;
  height: 40px;
  z-index: 999;
  margin: 20px 0;
  cursor: pointer;
}

.compass-outer-ring {
  position: absolute;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background: #fff;
}

.compass-inner-ring {
  position: absolute;
  top: 10%;
  left: 10%;
  width: 80%;
  height: 80%;
  border-radius: 50%;
  background: #fff;
  border: 2px solid rgb(243, 240, 37);
}

.compass-arrow {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 60%;
  height: 60%;
  transform-origin: center;
  transform: translate(-50%, -50%);
}

.compass-arrow-north {
  position: absolute;
  top: -30px;
  left: 50%;
  font-size: 14px;
  transform: translateX(-50%);
  color: #fff;
  font-weight: bold;
  text-shadow: 0 0 3px white;
}

.compass-arrow-west {
  position: absolute;
  top: 10%;
  left: -24px;
  font-size: 14px;
  transform: translateX(-50%);
  color: #fff;
  font-weight: bold;
  text-shadow: 0 0 3px white;
}

.compass-arrow-east {
  position: absolute;
  top: 10%;
  right: -29px;
  font-size: 14px;
  transform: translateX(-50%);
  color: #fff;
  font-weight: bold;
  text-shadow: 0 0 3px white;
}

.compass-arrow-south {
  position: absolute;
  bottom: -30px;
  left: 50%;
  font-size: 14px;
  transform: translateX(-50%);
  color: white;
  font-weight: bold;
  text-shadow: 0 0 3px black;
}

.compass-arrow-icon {
  position: absolute;
  top: 0;
  left: 50%;
  width: 0;
  height: 0;
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  border-bottom: 16px solid orange;
  transform: translate(-50%, -50%);
}
.compass-arrow-icon-bottom {
  content: " ";
  position: absolute;
  top: 100%;
  left: 50%;
  width: 0;
  height: 0;
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  border-top: 16px solid gray;
  transform: translate(-50%, -50%);
}

/* 长按旋转时的反馈效果 */
.compass-rotating {
  border: 2px solid yellow;
  box-shadow: 0 0 15px rgba(255, 255, 0, 0.7);
}

监听鼠标点击指南的事件,获取元素中点位置,并计算指南针中心点与鼠标的初始角度。

监听鼠标按下移动事件,实时计算指南针中心点与鼠标的角度,将角度转为弧度,并设置相机的heading值。

监听相机位置,同时旋转指南针的角度

const compassOuterRing = ref(null); //指南针元素
const isRotating = ref(false); //是否正在旋转
const startAngle = ref(0); //初始角度
const startHeading = ref(0); //初始弧度
const longPressTimer = ref(null); //计时
const longPressDuration = 300; // 长按判定时间(毫秒)

// 计算两点之间的角度
const calculateAngle = (center, point) => {
  return (Math.atan2(point.y - center.y, point.x - center.x) * 180) / Math.PI;
};

// 获取指南针的中心点坐标
const getElementCenter = (element) => {
  const rect = element.getBoundingClientRect();
  return {
    x: rect.left + rect.width / 2,
    y: rect.top + rect.height / 2,
  };
};

// 鼠标按下事件
const onMouseDown = (e) => {
  e.preventDefault();

  const center = getElementCenter(compassOuterRing.value);
  startAngle.value = calculateAngle(center, { x: e.clientX, y: e.clientY });
  startHeading.value = Cesium.Math.toDegrees(window.viewer.camera.heading);

  // 设置长按计时器
  longPressTimer.value = setTimeout(() => {
    isRotating.value = true;
    compassOuterRing.value.classList.add("compass-rotating");
  }, longPressDuration);
};

// 鼠标移动事件
const onMouseMove = (e) => {
  if (!isRotating.value) return;

  const center = getElementCenter(compassOuterRing.value);
  const currentAngle = calculateAngle(center, { x: e.clientX, y: e.clientY });
  const angleDiff = currentAngle - startAngle.value;

  const compassArrow = document.querySelector(".compass-arrow");
  compassArrow.style.transform = `translate(-50%, -50%) rotate(${angleDiff}deg)`;

  // 计算新的heading角度(转换为弧度)
  const newHeading = Cesium.Math.toRadians(startHeading.value - angleDiff);

  // 设置相机新的方向
  window.viewer.camera.setView({
    orientation: {
      heading: newHeading,
      pitch: window.viewer.camera.pitch,
      roll: window.viewer.camera.roll,
    },
  });
};

// 鼠标松开事件
const onMouseUp = () => {
  // 清除长按计时器
  clearTimeout(longPressTimer.value);

  if (isRotating.value) {
    isRotating.value = false;
    compassOuterRing.value.classList.remove("compass-rotating");
  }
};
//单击回正
const handleClick = () => {
  if (isRotating.value) return; // 如果正在旋转,不执行点击事件
  window.viewer.camera.setView({
    orientation: {
      heading: 0, // 朝北
      pitch: window.viewer.camera.pitch,
      roll: window.viewer.camera.roll,
    },
  });
  const compassArrow = document.querySelector(".compass-arrow");
  compassArrow.style.transform = `translate(-50%, -50%) rotate(${0}deg)`;
};

onMounted(() => {
  // 添加全局鼠标事件
  document.addEventListener("mousemove", onMouseMove);
  document.addEventListener("mouseup", onMouseUp);
  document.addEventListener("mouseleave", onMouseUp);
});

// 清理
onBeforeUnmount(() => {
  document.removeEventListener("mousemove", onMouseMove);
  document.removeEventListener("mouseup", onMouseUp);
  document.removeEventListener("mouseleave", onMouseUp);
});

//监听相机事件
viewer.scene.camera.changed.addEventListener(function () {
    updateCompass();
});

// 更新罗盘方向
const updateCompass = () => {
  // 获取罗盘DOM元素
  const compassArrow = document.querySelector(".compass-arrow");
  // 获取相机当前的heading角度(弧度)
  const heading = window.viewer.camera.heading;

  // 将弧度转换为角度(注意:Cesium的heading是顺时针方向,而CSS旋转是逆时针方向)
  const headingDegrees = Cesium.Math.toDegrees(heading);

  // 旋转罗盘箭头,使其指向北方
  compassArrow.style.transform = `translate(-50%, -50%) rotate(${-headingDegrees}deg)`;
};

点击罗盘移动鼠标,地图会跟着旋转。 f0e306fc3e2732a84cd814511e20783d.png