vue3+Cesium png、jpg格式图片根据图片四个点经纬度贴图【图片不拉伸】及旋转

不得不说,AI是真的强大,这篇代码全是AI写的,当然也不是一次写成功,cursor询问+修改大概20次改好,请看效果图

Video_20250616154259

这里的Cesium版本是1.116.0,下面的代码是组件ImageLoad,将这个组件加载到页面上,就可以执行贴图和旋转功能,imageUrl自己引入,图片四个角的经纬度这里是写成固定的数值

<template>
  <div :class="[prefixCls, 'shapefile-container mr-8px']">
    <BaseButton type="primary" @click="handleImageLoad">贴图</BaseButton>
    <BaseButton type="success" @click="testWithNetworkImage" style="margin-top: 10px">测试网络图片</BaseButton>
    <BaseButton type="warning" @click="testLocalImage" style="margin-top: 10px">测试本地生成图片</BaseButton>
    <BaseButton type="info" @click="showAllEntities" style="margin-top: 10px">显示所有实体</BaseButton>
    <BaseButton type="warning" @click="goToChina" style="margin-top: 10px">定位到中国</BaseButton>

    <!-- 修改旋转控制面板,移除状态显示 -->
    <div v-if="showRotationControl" class="rotation-panel">
      <div class="control-section">
        <h3>图片旋转控制</h3>
        <div class="rotation-controls">
          <el-button @click="rotate(-5)">-5°</el-button>
          <el-slider 
            v-model="rotationAngle" 
            :min="0" 
            :max="360" 
            @input="handleRotationChange"
          />
          <el-button @click="rotate(5)">+5°</el-button>
        </div>
        <div class="angle-display">当前角度:{{ rotationAngle }}°</div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { onBeforeUnmount, ref, onMounted, computed, ComputedRef } from 'vue';
import { ElIcon, ElMessage, ElButton, ElSlider } from 'element-plus';
import { Location, Crop, RefreshRight, Aim, DataLine, ArrowRightBold, ArrowRight } from '@element-plus/icons-vue';
import { useDesign } from '@/hooks/web/useDesign';
import { useCesiumStore } from '@/store/modules/cesium';
import TargetDetection from './TargetDetection.vue';
import { DataTree } from '@/views/RemoteSensing/components/DataRetrievalTree.vue';

const cesiumStore = useCesiumStore();
const { getPrefixCls } = useDesign();
const prefixCls = getPrefixCls('shapefile-uploader');

const isCollapsed = ref(false);
const showTargetDetection = ref(false);
const showRotationControl = ref(false);
const rotationAngle = ref(0);
const currentImageEntity = ref<any>(null);
const originalModelMatrix = ref<any>(null); // 保存原始的ModelMatrix
const centerPosition = ref<any>(null); // 保存旋转中心点

// 添加缺少的变量定义
let currentPrimitive: any = null; // 当前的Primitive对象

const linkInfo: ComputedRef<DataTree> = computed(() => cesiumStore.getCurLinkInfo);

// 新增:移除当前的Primitive
const removePrimitive = () => {
  if (currentPrimitive && window.GlobalViewer) {
    console.log('移除Primitive');
    try {
      window.GlobalViewer.scene.primitives.remove(currentPrimitive);
    } catch (error) {
      console.warn('移除Primitive时出错:', error);
    }
    currentPrimitive = null;
  }
};

// 修改创建图片实体的函数,支持旋转
const createNonStretchedImageEntity = (imageUrl: string, coordinates: any) => {
  return new Promise((resolve, reject) => {
    // 计算中心点
    const centerLon = (coordinates.leftTop.lon + coordinates.rightBottom.lon) / 2;
    const centerLat = (coordinates.leftTop.lat + coordinates.rightBottom.lat) / 2;
    centerPosition.value = Cesium.Cartesian3.fromDegrees(centerLon, centerLat, 0);

    const positions = [
      Cesium.Cartesian3.fromDegrees(coordinates.leftBottom.lon, coordinates.leftBottom.lat),
      Cesium.Cartesian3.fromDegrees(coordinates.rightBottom.lon, coordinates.rightBottom.lat),
      Cesium.Cartesian3.fromDegrees(coordinates.rightTop.lon, coordinates.rightTop.lat),
      Cesium.Cartesian3.fromDegrees(coordinates.leftTop.lon, coordinates.leftTop.lat),
    ];

    Cesium.Resource.fetchImage(imageUrl)
      .then((image) => {
        // 简化纹理坐标(不做宽高比调整)
        const textureCoordinates = new Float32Array([
          0, 0, // 左下
          1, 0, // 右下
          1, 1, // 右上
          0, 1, // 左上
        ]);

        const geometry = new Cesium.Geometry({
          attributes: {
            position: new Cesium.GeometryAttribute({
              componentDatatype: Cesium.ComponentDatatype.DOUBLE,
              componentsPerAttribute: 3,
              values: Cesium.Cartesian3.packArray(positions),
            }),
            st: new Cesium.GeometryAttribute({
              componentDatatype: Cesium.ComponentDatatype.FLOAT,
              componentsPerAttribute: 2,
              values: textureCoordinates,
            }),
          },
          indices: new Uint16Array([0, 1, 2, 0, 2, 3]),
          primitiveType: Cesium.PrimitiveType.TRIANGLES,
          boundingSphere: Cesium.BoundingSphere.fromPoints(positions),
        });

        const material = new Cesium.Material({
          fabric: {
            type: 'Image',
            uniforms: {
              image: imageUrl,
            },
          },
        });

        const instance = new Cesium.GeometryInstance({
          geometry: geometry,
          id: 'customImageOverlay',
        });

        // 保存原始ModelMatrix
        originalModelMatrix.value = Cesium.Matrix4.IDENTITY.clone();

        const primitive = new Cesium.Primitive({
          geometryInstances: instance,
          appearance: new Cesium.MaterialAppearance({
            material: material,
            translucent: true,
            flat: true,
          }),
          asynchronous: false,
          modelMatrix: originalModelMatrix.value
        });

        window.GlobalViewer.scene.primitives.add(primitive);
        resolve(primitive);
      })
      .catch(reject);
  });
};

// 备选方案:使用四元数来实现2D平面旋转
const applyRotationToMatrix = (angle: number) => {
  if (!currentPrimitive || !centerPosition.value) return;

  const angleRadians = Cesium.Math.toRadians(angle);
  
  // 获取地面的向上方向(法向量)
  const ellipsoid = Cesium.Ellipsoid.WGS84;
  const normal = ellipsoid.geodeticSurfaceNormal(centerPosition.value);
  
  // 使用四元数创建围绕法向量的旋转
  const quaternion = Cesium.Quaternion.fromAxisAngle(normal, angleRadians);
  const rotationMatrix = Cesium.Matrix3.fromQuaternion(quaternion);
  
  // 创建4x4变换矩阵
  const transform = Cesium.Matrix4.fromRotationTranslation(rotationMatrix, centerPosition.value);
  
  // 创建平移到原点的矩阵
  const centerToOrigin = Cesium.Matrix4.fromTranslation(
    Cesium.Cartesian3.negate(centerPosition.value, new Cesium.Cartesian3())
  );
  
  // 组合变换
  const combinedMatrix = Cesium.Matrix4.multiply(
    transform,
    centerToOrigin,
    new Cesium.Matrix4()
  );
  
  // 应用变换
  currentPrimitive.modelMatrix = combinedMatrix;
};

// 添加变量来保存当前图片URL和原始坐标
const currentImageUrl = ref<string>('');
const originalCoordinates = ref<any>(null);

// 修改handleImageLoad函数,保存必要信息
const handleImageLoad = async () => {
  console.log('开始贴图...');

  if (!window.GlobalViewer) {
    console.error('GlobalViewer 不存在');
    ElMessage.error('地图未初始化');
    return;
  }

  // 原始不规则四边形坐标
  const coordinates = {
    leftTop: { lon: 116.412, lat: 31.696 },
    rightTop: { lon: 116.418, lat: 31.705 },
    rightBottom: { lon: 116.508, lat: 31.6605 },
    leftBottom: { lon: 116.5022, lat: 31.6516 },
  };

  const imageUrl = '/testJpgPng/2018_1119_3m.jpg';

  // 保存当前信息
  currentImageUrl.value = imageUrl;
  originalCoordinates.value = coordinates;

  try {
    // 先清理旧的实体和Primitive
    if (currentImageEntity.value) {
      console.log('移除旧实体');
      window.GlobalViewer.entities.remove(currentImageEntity.value);
      currentImageEntity.value = null;
    }
    
    if (currentPrimitive) {
      console.log('移除旧Primitive');
      window.GlobalViewer.scene.primitives.remove(currentPrimitive);
      currentPrimitive = null;
    }

    // 重置旋转角度
    rotationAngle.value = 0;

    // 创建新的贴图
    const primitive = await createNonStretchedImageEntity(imageUrl, coordinates);
    currentPrimitive = primitive;

    // 只在第一次创建时定位
    if (!showRotationControl.value) {
      const centerLon = (coordinates.leftTop.lon + coordinates.rightBottom.lon) / 2;
      const centerLat = (coordinates.leftTop.lat + coordinates.rightBottom.lat) / 2;

      setTimeout(() => {
        window.GlobalViewer.camera.flyTo({
          destination: Cesium.Cartesian3.fromDegrees(centerLon, centerLat, 5000),
          orientation: {
            heading: Cesium.Math.toRadians(0),
            pitch: Cesium.Math.toRadians(-45),
            roll: 0.0,
          },
          duration: 2.0,
        });
      }, 500);
    }

    showRotationControl.value = true;
    ElMessage.success('图片贴图成功');

  } catch (error) {
    console.error('贴图失败:', error);
    ElMessage.error('贴图失败: ' + error.message);
  }
};

// 移除防抖处理,直接实时更新
const handleRotationChange = (value: number) => {
  rotationAngle.value = value;
  applyRotationToMatrix(value); // 直接更新,无需防抖
};

// 简化按钮旋转函数
const rotate = (delta: number) => {
  rotationAngle.value = (rotationAngle.value + delta + 360) % 360;
  applyRotationToMatrix(rotationAngle.value);
};

// 修改testLocalImage函数,也保存必要信息
const testLocalImage = () => {
  console.log('测试本地图片...');

  if (!window.GlobalViewer) {
    ElMessage.error('地图未初始化');
    return;
  }

  const coordinates = {
    leftTop: { lon: 116.412, lat: 31.696 },
    rightTop: { lon: 116.418, lat: 31.705 },
    rightBottom: { lon: 116.508, lat: 31.6605 },
    leftBottom: { lon: 116.5022, lat: 31.6516 },
  };

  // 测试一个简单的颜色图片
  const canvas = document.createElement('canvas');
  canvas.width = 256;
  canvas.height = 256;
  const ctx = canvas.getContext('2d');

  // 创建一个渐变测试图
  const gradient = ctx.createLinearGradient(0, 0, 256, 256);
  gradient.addColorStop(0, '#ff0000');
  gradient.addColorStop(0.5, '#00ff00');
  gradient.addColorStop(1, '#0000ff');

  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, 256, 256);

  // 添加一些文字
  ctx.fillStyle = 'white';
  ctx.font = '20px Arial';
  ctx.fillText('Test Image', 80, 130);

  const testImageUrl = canvas.toDataURL('image/png');

  // 保存当前信息
  currentImageUrl.value = testImageUrl;
  originalCoordinates.value = coordinates;

  try {
    removePrimitive();
    if (currentImageEntity.value) {
      window.GlobalViewer.entities.remove(currentImageEntity.value);
      currentImageEntity.value = null;
    }

    createNonStretchedImageEntity(testImageUrl, coordinates)
      .then((primitive) => {
        currentPrimitive = primitive;
        showRotationControl.value = true;
        window.GlobalViewer.camera.flyTo({
          destination: Cesium.Cartesian3.fromDegrees(116.45, 31.67, 5000),
          duration: 2.0,
        });
        ElMessage.success('测试图片贴图成功');
      })
      .catch((error) => {
        console.error('测试贴图失败:', error);
        ElMessage.error('测试贴图失败');
      });
  } catch (error) {
    console.error('测试贴图失败:', error);
    ElMessage.error('测试贴图失败');
  }
};

// 添加一个测试方法,用网络图片测试
const testWithNetworkImage = () => {
  console.log('使用网络图片测试...');

  if (!window.GlobalViewer) {
    ElMessage.error('地图未初始化');
    return;
  }

  // 使用一个可靠的网络图片进行测试
  const testImageUrl = 'https://cesium.com/img/cesiumjs/cesium-icon.png';

  const coordinates = {
    leftTop: { lon: 116.412, lat: 31.696 },
    rightTop: { lon: 116.418, lat: 31.705 },
    rightBottom: { lon: 116.508, lat: 31.6605 },
    leftBottom: { lon: 116.5022, lat: 31.6516 },
  };

  try {
    if (currentImageEntity.value) {
      console.log('remove', window.GlobalViewer.entities);
      window.GlobalViewer.entities.remove(currentImageEntity.value);
      currentImageEntity.value = null;
    }

    const hierarchy = new Cesium.PolygonHierarchy([
      Cesium.Cartesian3.fromDegrees(coordinates.leftBottom.lon, coordinates.leftBottom.lat),
      Cesium.Cartesian3.fromDegrees(coordinates.leftTop.lon, coordinates.leftTop.lat),
      Cesium.Cartesian3.fromDegrees(coordinates.rightTop.lon, coordinates.rightTop.lat),
      Cesium.Cartesian3.fromDegrees(coordinates.rightBottom.lon, coordinates.rightBottom.lat),
    ]);

    currentImageEntity.value = window.GlobalViewer.entities.add({
      name: '测试贴图',
      polygon: {
        hierarchy: hierarchy,
        material: new Cesium.ImageMaterialProperty({
          image: testImageUrl,
        }),
        height: 0,
        heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
      },
    });

    window.GlobalViewer.zoomTo(currentImageEntity.value);
    ElMessage.success('测试图片贴图成功');
  } catch (error) {
    console.error('测试贴图失败:', error);
    ElMessage.error('测试贴图失败');
  }
};

// 添加调试方法
const showAllEntities = () => {
  if (window.GlobalViewer) {
    console.log('当前所有实体:', window.GlobalViewer.entities.values);
    console.log('实体数量:', window.GlobalViewer.entities.values.length);

    if (window.GlobalViewer.entities.values.length > 0) {
      window.GlobalViewer.zoomTo(window.GlobalViewer.entities);
    }
  }
};

const goToChina = () => {
  if (window.GlobalViewer) {
    // 定位到中国区域
    window.GlobalViewer.camera.flyTo({
      destination: Cesium.Cartesian3.fromDegrees(116.4074, 39.9042, 1000000), // 北京上空1000公里
      duration: 3.0,
    });
  }
};

// 修改清理函数,移除定时器相关代码
onBeforeUnmount(() => {
  console.log('ImageLoad 组件卸载');
  
  // 清理实体
  if (currentImageEntity.value && window.GlobalViewer) {
    try {
      window.GlobalViewer.entities.remove(currentImageEntity.value);
      currentImageEntity.value = null;
    } catch (error) {
      console.error('清理实体失败:', error);
    }
  }
  
  // 清理Primitive
  removePrimitive();
  
  // 清理引用
  originalModelMatrix.value = null;
  centerPosition.value = null;
});

// 修改相机移动事件监听
onMounted(() => {
  console.log('ImageLoad 组件已挂载');
});
</script>

<style lang="scss" scoped>
$prefix-cls: '#{$namespace}-shapefile-uploader';

// 添加一个外层容器类
.shapefile-container {
  position: absolute;
  top: 120px;
  right: 20px;
  &.collapsed {
    width: 0 !important;
  }
}

.collapse-trigger {
  position: absolute;
  left: -20px;
  top: 20px;
  width: 14px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: all 0.3s;

  .trigger-text {
    writing-mode: vertical-lr;
    padding: 8px 6px;
    background-color: var(--el-color-primary);
    color: #fff;
    // border-radius: 4px 0 0 4px;
    font-size: 14px;
    display: flex;
    flex-direction: row;
    align-items: center;

    .text-char {
      margin-top: 2px;
    }
  }

  .trigger-icon {
    width: 14px;
    height: 30px;
    background-color: var(--el-color-primary);
    color: #fff;
    border-radius: 2px;
    display: flex;
    align-items: center;
    justify-content: center;

    :deep(.el-icon) {
      font-size: 12px;
      color: #fff;
    }
  }
}

.content-wrapper {
  background-color: var(--el-bg-color);
  border-radius: 8px;
  box-shadow: var(--el-box-shadow-light);
  height: 100%;
}

// 添加过渡动画
.fade-slide-enter-active,
.fade-slide-leave-active {
  transition: all 0.3s ease-in-out;
}

.fade-slide-enter-from,
.fade-slide-leave-to {
  opacity: 0;
  transform: translateX(30px);
}

.#{$prefix-cls} {
  background-color: var(--el-bg-color);
  border-radius: 8px;
  box-shadow: var(--el-box-shadow-light);

  &__header {
    padding: 16px 20px;
    border-bottom: 1px solid var(--el-border-color-light);
  }

  &__title {
    margin: 0;
    font-size: 16px;
    font-weight: 500;
    color: var(--el-text-color-primary);
  }

  &__content {
    padding: 12px;

    .tool-list {
      list-style: none;
      padding: 0;
      margin: 0;
      width: 100%;
    }

    .tool-item {
      width: calc(100% - 24px - 2px);
      height: 88px;
      position: relative;
      display: flex;
      padding: 12px;
      margin-bottom: 12px;
      border-radius: 8px;
      border: 1px solid var(--el-fill-color-dark);
      cursor: pointer;
      transition: all 0.5s;
      align-items: center;

      .tool-icon {
        display: flex;
        align-items: center;
        justify-content: center;
        width: 40px;
        height: 40px;
        margin-right: 12px;
        border-radius: 8px;
        background: var(--el-color-primary-light-9);

        :deep(.el-icon) {
          font-size: 24px;
          color: var(--el-color-primary);
        }
      }

      .tool-info {
        flex: 1;
      }

      .tool-title {
        display: flex;
        align-items: center;
        font-size: 14px;
        font-weight: 500;
        color: var(--el-text-color-primary);
        margin-bottom: 4px;

        .tool-badge {
          margin-left: 8px;
          padding: 2px 6px;
          font-size: 12px;
          border-radius: 4px;
          background: var(--el-color-primary-light-8);
          color: var(--el-color-primary);
        }
      }

      .tool-desc {
        font-size: 12px;
        color: var(--el-text-color-secondary);
        line-height: 1.4;
      }

      .tool-arrow {
        position: absolute;
        right: 12px;
        top: 50%;
        transform: translateY(-50%);
        width: 14px;
        height: 24px;
        // border-radius: 50%;
        background-color: var(--el-color-primary);
        display: flex;
        align-items: center;
        justify-content: center;
        opacity: 0;
        transition: all 0.5s;
        border-radius: 4px;

        :deep(.el-icon) {
          font-size: 10px;
          color: #fff;
        }
      }

      &:hover {
        border-color: var(--el-color-primary);

        .tool-arrow {
          opacity: 1;
          right: 8px;
        }
      }
    }
  }
}

.rotation-panel {
  position: absolute;
  top: 50px;
  right: 0;
  background: var(--el-bg-color);
  padding: 15px;
  border-radius: 8px;
  box-shadow: var(--el-box-shadow-light);
  width: 300px;
  z-index: 1000;

  .control-section {
    h3 {
      margin: 0 0 10px 0;
      font-size: 14px;
      color: var(--el-text-color-primary);
    }

    .rotation-controls {
      display: flex;
      align-items: center;
      gap: 10px;
      margin-bottom: 10px;

      :deep(.el-slider) {
        flex: 1;
      }
    }

    .angle-display {
      text-align: center;
      color: var(--el-text-color-secondary);
      font-size: 12px;
    }
  }
}
</style>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值