Cesium三维地图和leaflet二维地图卷帘分屏联动

效果图1:

效果图2:

cesium和leaflet用的都是同一个影像图层,中间的红色分屏线可以左右滑动,但是目前只能是左边的cesium图层控制右边的leaflet图层,反之不行(还未做该功能),如果分屏线离中间越远,则分屏线两边相差越大,如下图所示:

实现原理:左右两边其实都是100%宽度,通过css的clip-path属性设置了各自的显示范围,通过cesium的屏幕中心点设置leaflet的地图中心点,从而实现联动,但是由于leaflet和cesium的缩放层级无法控制一致,zoom的层级定义不一样,所以这个层级是通过微调实现大体一致,缩放过曾中辉看到由于层级的不一样导致左右两边相差很大,如下图所示:

javascript 复制代码
<template>
  <div class="el-main">
    <!-- Cesium 容器 -->
    <div id="cesiumContainer"></div>
    <!-- 卷帘滑块 -->
    <div id="slider"></div>
    <!-- Leaflet 地图容器 -->
    <div id="leafletContainer"></div>
  </div>
</template>

<script setup>
import { onMounted, ref } from "vue";
import * as Cesium from "cesium";
import L from "leaflet"; // 引入 Leaflet

// 设置 Cesium Ion 的默认访问令牌
Cesium.Ion.defaultAccessToken = "youKey";

onMounted(() => {
  init();
});
let leafletUpdating = ref(false); // 标志位,防止循环触发
let leafletMap;
const init = () => {
  const viewer = initializeCesiumViewer();
  const leafletMap = initializeLeafletMap();
  const slider = document.getElementById("slider");
  // 监听cesium相机变化事件
  viewer.camera.percentageChanged = 0.00001;
  viewer.camera.changed.addEventListener(() => {
    leafletUpdating.value = false;
    const centerPosition = getCenterPosition(viewer);
    if (centerPosition) {
      const zoomLevel = calculateZoomLevel(
        viewer.camera.positionCartographic.height
      );
      leafletMap.setView(
        [centerPosition.lat, centerPosition.lng],
        zoomLevel + 4
      );
      leafletUpdating.value = true;
    }
  });
  // 监听leaflet相机变化事件

  let splitPosition = 0.5; // 初始分屏位置为 50%
  updateSplitPosition(splitPosition);

  const handler = new Cesium.ScreenSpaceEventHandler(slider);
  let moveActive = false;

  // 滑块移动逻辑
  const move = (movement) => {
    // 如果没有激活移动状态,则直接返回
    if (!moveActive) return;

    // 获取滑块父元素的宽度
    const parentWidth = slider.parentElement.offsetWidth;

    // 计算新的分割位置,确保其在0到1之间
    const newSplitPosition = Math.min(
      // slider.offsetLeft 获取滑块当前的左边距(以像素为单位)。
      // movement.endPosition.x 获取鼠标或触摸事件的移动距离(以像素为单位)。
      Math.max((slider.offsetLeft + movement.endPosition.x) / parentWidth, 0),
      1
    );

    // 更新分割位置
    updateSplitPosition(newSplitPosition);
  };

  // 监听鼠标按下事件(开始拖动)
  handler.setInputAction(() => {
    moveActive = true;
  }, Cesium.ScreenSpaceEventType.LEFT_DOWN);

  // 监听鼠标移动事件(拖动过程中更新分屏位置)
  handler.setInputAction(move, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

  // 监听鼠标松开事件(结束拖动)
  handler.setInputAction(() => {
    moveActive = false;
  }, Cesium.ScreenSpaceEventType.LEFT_UP);
  // 监听 Leaflet 的 zoomend 事件
  leafletMap.on("zoomend", (e) => {
    const center = leafletMap.getCenter();
    const scale = e.target.getZoom();
    console.log("leaflet坐标和层级:", center, scale);
  });

  // 将 viewer 挂载到全局 window 对象上,便于调试
  window.viewer = viewer;
};

/**
 * 初始化 Cesium Viewer 实例
 * @returns {Cesium.Viewer} Cesium Viewer 实例
 */
const initializeCesiumViewer = () => {
  const viewer = new Cesium.Viewer("cesiumContainer", {
    infoBox: false,
    geocoder: false,
    timeline: false,
    animation: false,
  });
  viewer.camera.setView({
    destination: Cesium.Cartesian3.fromDegrees(
      116.38225078582765,
      39.90710270565395,
      4500
    ),
    orientation: {
      heading: 6.283185307179581,
      pitch: -1.5688168484696687,
      roll: 0.0,
    },
  });
  var layer = new Cesium.UrlTemplateImageryProvider({
    url: "http://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}",
    minimumLevel: 4,
    maximumLevel: 18,
  });
  viewer.imageryLayers.addImageryProvider(layer);
  viewer.camera.constrainedAxis = Cesium.Cartesian3.UNIT_Z; // 约束相机只能垂直缩放
  viewer.scene.screenSpaceCameraController.zoomFactor = 0.1; // 设置缩放速率
  return viewer;
};

/**
 * 初始化 Leaflet 地图
 * @returns {L.Map} Leaflet 地图实例
 */
const initializeLeafletMap = () => {
  leafletMap = L.map("leafletContainer").setView(
    [39.90710270565395, 116.38225078582765],
    15
  );
  const layer = L.tileLayer(
    "http://webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}",
    {
      subdomains: ["1", "2", "3", "4"],
      minZoom: 1,
      maxZoom: 19,
    }
  );
  leafletMap.addLayer(layer);
  return leafletMap;
};

/**
 * 更新分屏位置
 * @param {number} position - 分屏位置(0 到 1 之间的值)
 */
const updateSplitPosition = (position) => {
  const slider = document.getElementById("slider");
  slider.style.left = `${100 * position}%`;
  document.getElementById("leafletContainer").style.width = `${
    100 * (1 - position)
  }%`;
  document.getElementById("leafletContainer").style.clipPath = `inset(0 0 0 ${
    100 * position
  }%)`;
  document.getElementById("cesiumContainer").style.width = `${100 * position}%`;
  document.getElementById("cesiumContainer").style.clipPath = `inset(0 ${
    100 * (1 - position)
  }% 0 0)`;
};

/**
 * 获取屏幕中心点的地理坐标
 * @param {Cesium.Viewer} viewer - Cesium Viewer 实例
 * @returns {Object|null} 包含经度和纬度的对象,或 null
 */
const getCenterPosition = (viewer) => {
  const centerResult = viewer.camera.pickEllipsoid(
    new Cesium.Cartesian2(
      viewer.canvas.clientWidth / 2,
      viewer.canvas.clientHeight / 2
    )
  );
  if (centerResult) {
    const cartographic =
      Cesium.Ellipsoid.WGS84.cartesianToCartographic(centerResult);
    return {
      lng: Cesium.Math.toDegrees(cartographic.longitude),
      lat: Cesium.Math.toDegrees(cartographic.latitude),
    };
  }
  return null;
};

/**
 * 计算近似的缩放级别
 * @param {number} height - 相机高度(米)
 * @returns {number} 缩放级别
 */
const calculateZoomLevel = (height) => {
  const earthRadius = 6378137; // 地球半径(米)
  return Math.round(Math.log2((2 * earthRadius) / height));
};
// 将Leaflet的缩放级别转换为Cesium的相机高度
const calculateHeightFromZoom = (zoomLevel) => {
  const earthRadius = 6378137; // 地球半径(米)
  return (2 * earthRadius) / Math.pow(2, zoomLevel);
};
</script>

<style scoped>
/* 卷帘滑块样式 */
#slider {
  position: absolute;
  left: 50%;
  top: 0;
  background-color: red;
  width: 3px;
  height: 100%;
  cursor: ew-resize;
  z-index: 10;
}

/* Leaflet 地图容器样式 */
#leafletContainer {
  position: absolute;
  top: 0;
  bottom: 0;
  right: 0;
  width: 100% !important;
  clip-path: inset(0 0 0 50%);
}

/* Cesium 地图容器样式 */
#cesiumContainer {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  /* width: 50%; */
  z-index: 10;
  width: 100% !important;
  clip-path: inset(0 50% 0 0);
}

/* 主内容区域样式 */
.el-main {
  position: relative;
  width: 100%;
  height: 100%;
}
</style>
相关推荐
爱编程的鱼23 分钟前
Unity—从入门到精通(第一天)
前端·unity·ue5·游戏引擎
默默无闻 静静学习24 分钟前
sass介绍
前端·sass
大怪v1 小时前
前端佬们,装起来!给设计模式【祛魅】
前端·javascript·设计模式
sunly_1 小时前
Flutter:页面滚动,导航栏背景颜色过渡动画
开发语言·javascript·flutter
vvilkim1 小时前
Vue.js 插槽(Slot)详解:让组件更灵活、更强大
前端·javascript·vue.js
学无止境鸭1 小时前
uniapp报错 Right-hand side of ‘instanceof‘ is not an object
前端·javascript·uni-app
豆豆(设计前端)1 小时前
一键秒连WiFi智能设备,uni-app全栈式物联开发指南。
前端
Aphasia3112 小时前
🧑🏻‍💻前端面试高频考题(万字长文📖)
前端·面试
程序饲养员2 小时前
Javascript中export后该不该加default?
前端·javascript·前端框架
腥臭腐朽的日子熠熠生辉2 小时前
nvm 安装某个node.js版本后不能使用或者报错,或不能使用npm的问题
前端·npm·node.js