three.js+vue3三维地图下钻地图,实现下钻全国-》省份-》城市-》区县

案例效果截图:

具体场景和功能,详见b站视频:

https://www.bilibili.com/video/BV1Kb421q7c4/?vd_source=7d4ec9c9275b9c7d16afe9b4625f636c

 案例逻辑代码:

<template>
  <div id="chinaMap">
    <div id="threejs" ref="threejs"></div>
    <!-- 右侧按钮 -->
    <div class="rightButton" ref="rightButton">
      <div v-for="(item, index) in rightButItem" :key="index" :value="item.value"
        :class="item.selected ? 'selected common' : 'common'" @click="rightButClick">
        {{ item.name }}
      </div>
    </div>
    <!-- 地图名称元素 -->
    <div id="provinceName" style="display: none"></div>
    <!-- 光柱上方数值元素 -->
    <div id="cylinderValue" style="display: none"></div>
    <!-- 地图标牌元素 -->
    <div id="mapTag" style="display: none">
      <div class="content">
        <div>旅客:</div>
        <div id="mapTag_value">1024万</div>
      </div>
      <div class="arrow"></div>
    </div>
    <!-- 弹框元素 -->
    <div id="popup" style="display: none">
      <div class="popup_line"></div>
      <div class="popup_Main">
        <div class="popupMain_top"></div>
        <div class="popup_content">
          <div class="popup_head">
            <div class="popup_title">
              <div class="title_icon"></div>
              <div id="popup_Name">湖北省</div>
            </div>
            <div class="close" @click="popupClose"></div>
          </div>
          <div class="popup_item">
            <div>当前流入:</div>
            <div class="item_value">388万人次</div>
          </div>
          <div class="popup_item">
            <div>景区容量:</div>
            <div class="item_value">2688万人次</div>
          </div>
          <div class="popup_item">
            <div>交通资源利用率:</div>
            <div class="item_value">88.7%</div>
          </div>
          <div class="popup_item">
            <div>省市热搜指数:</div>
            <div class="item_value">88.7%</div>
          </div>
        </div>
        <div class="popupMain_footer"></div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { regionCode } from '@/assets/regionCode.js';
import { onMounted, reactive, computed, ref } from 'vue';
import * as THREE from 'three';
// 引入TWEENJS
import TWEEN from '@tweenjs/tween.js';
import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
import { CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js';
// threejs基础配置,场景相机渲染器等
import { scene, camera, controls, renderer, css3DRenderer, css2DRenderer, outlinePass, composer, finalComposer } from './baseConfig/index.js';
// 加载地图
import { initMap, nationMapModel, mapUf, projection, topMaterial, sideMaterial } from './initChinaMap/index.js';
// 地图底部网格背景
import { gridHelper, meshPoint } from './backgroundMesh/index.js';
// 初始化鼠标移入地图浮动效果
import { initMapFloat } from './mapFloat/index.js';
// 外圈、内圈、扩散波纹、渐变平面
import { circleUf, outerCircle, innerCircle, diffuseCircle, gradientPlane } from './backgroundCircle/index.js';
// 光柱发光数组、光圈动画、创建光柱函数
import { cylinderGlowArr, apertureAnimation, createCylinder } from './cylinder/index.js';
// 创建地图标牌函数
import { createMapTag } from './mapTag/index.js';
import { particlesUpdate, createParticles, particles } from './particles/index.js';
// 首次进入动画
import { eventAnimation } from './eventAnimation/index.js';

// 右侧按钮选项
const rightButItem = reactive([
  { value: 'cylinder', name: '光柱', selected: false },
  { value: 'tag', name: '标牌', selected: false },
]);
// 描边模型
let outlineModel = null;
// 时钟对象,用于获取两帧渲染之间的时间值
const clock = new THREE.Clock();
// 射线拾取中模型对象
let rayObj = null;
// 弹框div元素
let popupDiv = null;
// css2D弹框对象
let css2Dpopup = null;
// 需要辉光的模型数组
let glowArr = [];
// 右侧按钮元素
const rightButton = ref();
// 地图状态,分为国省市三种,决定了点击事件等操作效果
const mapStatus = ref('国');
// 省份地图模型
const provinceMapModel = new THREE.Group();
provinceMapModel.name = '省';
provinceMapModel.rotateX(-Math.PI / 2);
// 城市级地图模型
const cityMapModel = new THREE.Group();
cityMapModel.name = '市';
cityMapModel.rotateX(-Math.PI / 2);
// 当前显示模型
const currentShowModel = computed(() => {
  return mapStatus.value === '国' ? nationMapModel : mapStatus.value === '省' ? provinceMapModel : cityMapModel;
});
// threejs容器元素
const threejs = ref();

onMounted(async () => {
  threejs.value.appendChild(renderer.domElement);
  threejs.value.appendChild(css3DRenderer.domElement);
  threejs.value.appendChild(css2DRenderer.domElement);

  // 初始化弹框
  initCSS2DPopup();
  // // 创建省份名称对象
  // createProvinceName();
  // 创建粒子
  createParticles();

  // 加载中国地图
  await initMap();
  // 初始化鼠标移入地图浮动效果
  initMapFloat(camera, currentShowModel);
  // 初始化点击事件
  initClickEvent();
  scene.add(nationMapModel, gridHelper, meshPoint, outerCircle, innerCircle, diffuseCircle, gradientPlane, provinceMapModel, cityMapModel, particles);
  // 设置需要辉光物体
  glowArr = [...cylinderGlowArr];
  // 开始循环渲染
  render();

  // 需要展示全国则注释掉regionSetMap函数调用
  // regionSetMap('湖北省')  

  // 首次进入动画
  eventAnimation(camera, controls);
});

// 循环渲染
function render() {
  requestAnimationFrame(render);
  camera.updateProjectionMatrix();
  controls.update();
  // 两帧渲染间隔
  let deltaTime = clock.getDelta();
  // 地图模型侧边渐变效果
  mapUf.uTime.value += deltaTime;
  if (mapUf.uTime.value >= 5) {
    mapUf.uTime.value = 0.0;
  }
  if (rightButItem[0].selected) apertureAnimation(); // 光圈缩放动画
  particlesUpdate(); // 粒子动画
  // 背景外圈内圈旋转
  outerCircle.rotation.z -= 0.003;
  innerCircle.rotation.z += 0.003;
  // 波纹扩散动画
  circleUf.uTime.value += deltaTime;
  if (circleUf.uTime.value >= 6) {
    circleUf.uTime.value = 0.0;
  }
  // TWEEN更新
  TWEEN.update();
  // 将场景内的物体材质设置为黑色
  scene.traverse(darkenMaterial);
  // 渲染辉光
  composer.render();
  // 还原材质
  scene.traverse(restoreMaterial);
  // 最终渲染
  finalComposer.render();
  css3DRenderer.render(scene, camera);
  css2DRenderer.render(scene, camera);
}

// 右侧按钮点击事件
function rightButClick(e, item) {
  // 当前按钮点击项
  let clickItem;
  //  代码中触发的点击项
  if (item) {
    clickItem = item;
  }
  // 用户手动点击触发的点击项
  else {
    const value = e.target.getAttribute('value');
    clickItem = rightButItem.filter((obj) => obj.value === value)[0];
    clickItem.selected = !clickItem.selected;
  }

  if (clickItem.selected) {
    currentShowModel.value.traverse((item) => {
      if (item.name === clickItem.name) {
        item.traverse((item) => (item.visible = true));
      }
    });
  } else {
    currentShowModel.value.traverse((item) => {
      if (item.name === clickItem.name) {
        item.traverse((item) => (item.visible = false));
      }
    });
  }
}

// 弹框关闭事件
function popupClose() {
  // outlineModel存在
  if (outlineModel) {
    outlinePass.selectedObjects = [];
    rayObj.remove(outlineModel);
    // 给弹框清除创建渐变动画
    new TWEEN.Tween({ opacity: 1 })
      .to({ opacity: 0 }, 500)
      .onUpdate(function (obj) {
        //动态更新div元素透明度
        popupDiv.style.opacity = obj.opacity;
      })
      .onComplete(function () {
        // 清除弹框
        rayObj.remove(css2Dpopup);
      })
      .start();
  }
}

// 将材质设置成黑色
function darkenMaterial(obj) {
  if (obj.visible) {
    if (obj instanceof THREE.Scene) {
      obj.background = null;
    }
    const material = obj.material;
    if (material && !glowArr.includes(obj) && !material.isShaderMaterial) {
      obj.originalMaterial = obj.material;
      const Proto = Object.getPrototypeOf(material).constructor;
      obj.material = new Proto({ color: new THREE.Color('#000') });
    }
  }
}

// 还原材质
function restoreMaterial(obj) {
  if (obj.visible) {
    if (!obj.originalMaterial) return;
    obj.material = obj.originalMaterial;
    delete obj.originalMaterial;
  }
}

// 初始化CSS2D弹框
function initCSS2DPopup() {
  popupDiv = document.getElementById('popup');
  const widthScale = window.innerWidth / 1920;
  const heightScale = window.innerHeight / 941;
  popupDiv.style.top += (37 * heightScale).toFixed(2) + 'px';
  popupDiv.style.left += (390 * widthScale).toFixed(2) + 'px';
  // 转换为CSS2D对象
  css2Dpopup = new CSS2DObject(popupDiv);
  css2Dpopup.name = '弹框';
  // 设置一个较高的渲染顺序,防止弹框被标牌等物体遮挡住
  css2Dpopup.renderOrder = 99;
}

// 初始化点击事件
function initClickEvent() {
  // 左键点击定时器
  let leftClickTimer = null;
  // 右键点击定时器
  let rightClickTimer = null;
  // 左键跟踪连续点击的次数
  let leftClickCount = 0;
  // 右键跟踪连续点击的次数
  let rightClickCount = 0;
  // 延迟时间
  const delay = 300;
  // 添加左键点击事件,双击进入下一层地图(国=>省=>市)
  addEventListener('click', function (event) {
    // 每次点击时增加点击次数
    leftClickCount++;
    // 如果已经有定时器在运行,重置定时器
    if (leftClickTimer) {
      clearTimeout(leftClickTimer);
    }
    // 设置定时器,如果用户停止点击,将根据点击次数决定触发何种事件
    leftClickTimer = setTimeout(() => {
      // 点击次数超过2次以上都视为双击
      if (leftClickCount >= 2) {
        // 当为市级地图时没有下一层了,跳出不执行后续的代码
        if (mapStatus.value === '市') return;
        // 射线检测
        const testResult = rayTest(event, currentShowModel.value);
        // 检测到模型时
        if (testResult.length) {
          // 双击处理
          doubleClickHandle(testResult, 'left');
        }
      }
      // 点击次数为1则为单击事件
      else {
        // 射线检测
        const testResult = rayTest(event, currentShowModel.value);
        // 检测到模型在进行下一步处理
        if (testResult.length) oneClickHandle(testResult);
        // 未检测到则触发弹框关闭事件
        // else popupClose();
      }
      // 重置计数器和定时器
      leftClickTimer = null;
      leftClickCount = 0;
    }, delay);
  });

  // 添加右键点击事件,双击返回上一层地图(市=>省=>国)
  addEventListener('contextmenu', function (event) {
    // 每次点击时增加点击次数
    rightClickCount++;

    // 如果已经有定时器在运行,重置定时器
    if (rightClickTimer) {
      clearTimeout(rightClickTimer);
    }
    // 设置定时器,如果用户停止点击,将根据点击次数决定触发何种事件
    rightClickTimer = setTimeout(() => {
      // 点击次数超过2次以上都视为双击
      if (rightClickCount >= 2) {
        // 当为国级地图时没有上一层了,跳出不执行后续的代码
        if (mapStatus.value === '国') return;
        // 双击处理
        doubleClickHandle(null, 'right');
      }
      // 重置计数器和定时器
      rightClickTimer = null;
      rightClickCount = 0;
    }, delay);
  });
}

// 射线检测
function rayTest(event, model) {
  const px = event.offsetX;
  const py = event.offsetY;
  // 屏幕坐标转为标准设备坐标
  const x = (px / window.innerWidth) * 2 - 1;
  const y = -(py / window.innerHeight) * 2 + 1;
  // 创建射线
  const raycaster = new THREE.Raycaster();
  // 设置射线参数
  raycaster.setFromCamera(new THREE.Vector2(x, y), camera);
  // 射线交叉计算拾取模型
  let intersects = raycaster.intersectObjects(model.children);
  // 检测结果过滤
  intersects = intersects.filter(function (intersect) {
    return intersect.object.name !== '边线' && intersect.object.name !== '地图名称' && intersect.object.name !== '光圈' && intersect.object.name !== '光柱' && intersect.object.name !== '标牌';
  });
  return intersects;
}

// 地图状态切换
function mapStatusSwitch(e) {
  // 地图状态数组
  const mapStatusArr = ['国', '省', '市'];
  // 当前地图状态在mapStatusArr中的下标位置
  let index = mapStatusArr.indexOf(mapStatus.value);
  // left表示鼠标左键双击触发,地图状态进入下一层,国=>省=>市
  if (e === 'left') {
    index++;
  }
  // right表示鼠标右键键双击触发,地图状态返回上一层,市=>省=>国
  else if (e === 'right') {
    index--;
  }
  // 重新定义地图状态
  if (mapStatusArr[index]) mapStatus.value = mapStatusArr[index];
}

// 单击处理
function oneClickHandle(rel) {
  // 清除上一次添加的描边模型
  if (rayObj) {
    rayObj.remove(outlineModel);
  }
  // 射线选中模型
  rayObj = rel[0].object.parent;

  // 添加弹框到选中模型rayObj中去
  rayObj.add(css2Dpopup);
  // 获取选中模型的位置
  const center = rayObj.userData.center;
  // 设置弹框位置
  css2Dpopup.visible = true;
  css2Dpopup.position.set(center[0], center[1], 0);
  // 弹框名称元素
  const popupNameDiv = css2Dpopup.element.querySelector('#popup_Name');
  // 更换弹框名称
  popupNameDiv.innerHTML = rayObj.name;
  // 给弹框显示创建渐变动画
  new TWEEN.Tween({ opacity: 0 })
    .to({ opacity: 1.0 }, 500)
    .onUpdate(function (obj) {
      //动态更新div元素透明度
      popupDiv.style.opacity = obj.opacity;
    })
    .start();

  // 地图边线数据
  const mapLineData = rayObj.userData.mapData;
  // 创建shape对象
  const shape = new THREE.Shape();
  // 当数据为多个多边形时
  if (mapLineData.type === 'MultiPolygon') {
    // 遍历数据,绘制shape对象数据
    mapLineData.coordinates.forEach((coordinate, index) => {
      if (index === 0) {
        coordinate.forEach((rows) => {
          rows.forEach((row) => {
            const [x, y] = projection(row);
            if (index === 0) {
              shape.moveTo(x, y);
            }
            shape.lineTo(x, y);
          });
        });
      }
    });
  }
  // 当数据为单个多边形时
  if (mapLineData.type === 'Polygon') {
    mapLineData.coordinates.forEach((coordinate) => {
      // 遍历数据,绘制shape对象数据
      mapLineData.coordinates.forEach((rows, index) => {
        if (index === 0) {
          rows.forEach((row) => {
            const [x, y] = projection(row);
            if (index === 0) {
              shape.moveTo(x, y);
            }
            shape.lineTo(x, y);
          });
        }
      });
    });
  }
  // 创建形状几何体,shape对象作为参数
  const geometry = new THREE.ShapeGeometry(shape);
  const material = new THREE.MeshBasicMaterial({
    color: rayObj.children[0].material[0].color,
    map: rayObj.children[0].material[0].map,
    side: THREE.DoubleSide,
  });
  let mesh = new THREE.Mesh(geometry, material);
  mesh.rotateX(-Math.PI);
  mesh.name = '描边模型';
  outlineModel = mesh;
  rayObj.add(outlineModel);
  // 设置描边模型进行发光
  outlinePass.selectedObjects = [outlineModel];
  glowArr.pop();
  glowArr.push(outlineModel);
}

function codeSetMapStatus(code) {
  const cityCode = code.substring(2, 4);
  const districtCode = code.substring(4, 6);
  if (code === "100000") {
    mapStatus.value = '国';
  } else if (cityCode === "00" && districtCode === "00") {
    mapStatus.value = '省';
  } else if (districtCode === "00") {
    mapStatus.value = '市';
  }
}

function regionSetMap(regionName) {
  // 隐藏地图状态切换之前的模型
  currentShowModel.value.traverse((item) => {
    item.visible = false;
  });
  const code = regionCode[regionName];
  codeSetMapStatus(code);
  let url = `http://211.143.122.110:18062/mapdata/geojson/areas_v3_full/all/${code}.json`;
  fetch(url)
    .then((response) => {
      if (!response.ok) {
        // 如果响应状态码不是2xx,抛出错误
        throw new Error('Network response was not ok: ' + response.statusText);
      }
      // 响应成功,返回解析的JSON数据
      return response.json();
    })
    .then(async (data) => {
      await new Promise((resolve) => {
        // 处理地图数据,绘制模型
        handleMapData(data, 1, resolve);
        // 全国模型的包围盒
        const nationModelBox = new THREE.Box3().setFromObject(nationMapModel);
        // 当前射线选中模型的包围盒
        let currentModelbox;
        // 市级地图切换到市级地图时,市级地图是经过了一次缩放的,需要还原缩放比例进行计算
        if (mapStatus.value === '市') {
          const cloneModel = currentShowModel.value.clone();
          cloneModel.scale.set(1, 1, 1);
          currentModelbox = new THREE.Box3().setFromObject(cloneModel);
        } else {
          currentModelbox = new THREE.Box3().setFromObject(currentShowModel.value);
        }
        // 计算宽度和高度
        const widthA = nationModelBox.max.x - nationModelBox.min.x;
        const heightA = nationModelBox.max.z - nationModelBox.min.z;
        const widthB = currentModelbox.max.x - currentModelbox.min.x;
        const heightB = currentModelbox.max.z - currentModelbox.min.z;
        // 计算宽度和高度的比例
        const widthRatio = widthA / widthB;
        const heightRatio = heightA / heightB;
        // 当前模型与全国模型大小的缩放值
        const scale = (widthRatio + heightRatio) / 2;
        // 应用缩放值到切换后的模型上去
        currentShowModel.value.scale.set(scale, scale, 1);
        currentShowModel.value.traverse(item => {
          if (item.name.includes('名称对象')) {
            item.scale.set(0.2 / scale, 0.2 / scale, 0.2 / scale);
          }

          if (item.name == '光柱') {
            item.scale.set(1 / scale, 1, 1 / scale)
          }
        })


        // 这时候计算省份模型,得出放大后的省份模型的中心点,并将其位置归于原点
        const scaledBox = new THREE.Box3().setFromObject(currentShowModel.value);
        const center = new THREE.Vector3();
        // 获取放大后模型的中心点
        scaledBox.getCenter(center);
        // 将模型的位置调整,使缩放后的中心位于原点
        currentShowModel.value.position.sub(center);
        // 高度不增加
        currentShowModel.value.position.y += center.y;
        // 显示地图状态切换之后的模型
        currentShowModel.value.traverse((item) => {
          item.visible = true;
        });
        glowArr = [...cylinderGlowArr];
        // 触发右侧按钮点击事件
        rightButItem.map((item) => {
          rightButClick(null, item);
        });


      });
    });

}

// 双击处理
function doubleClickHandle(rel, type) {
  console.log(rel, 'rel')
  // 隐藏地图状态切换之前的模型
  currentShowModel.value.traverse((item) => {
    item.visible = false;
  });

  // 左键双击进入地图下一层
  if (type === 'left') {
    // 地图状态切换,国=>省=>市
    mapStatusSwitch('left');
    // 射线检测到的模型
    const relModel = rel[0].object.parent;
    // 地图code,用于获取数据
    const adcode = relModel.userData.adcode;
    let url = `http://211.143.122.110:18062/mapdata/geojson/areas_v3_full/all/${adcode}.json`;
    fetch(url)
      .then((response) => {
        if (!response.ok) {
          // 如果响应状态码不是2xx,抛出错误
          throw new Error('Network response was not ok: ' + response.statusText);
        }
        // 响应成功,返回解析的JSON数据
        return response.json();
      })
      .then(async (data) => {
        await new Promise((resolve) => {
          // 全国模型的包围盒
          const nationModelBox = new THREE.Box3().setFromObject(nationMapModel);
          // 当前射线选中模型的包围盒
          let currentModelbox;
          // 市级地图切换到市级地图时,市级地图是经过了一次缩放的,需要还原缩放比例进行计算
          if (mapStatus.value === '市') {
            const cloneModel = relModel.clone();
            cloneModel.scale.set(1, 1, 1);
            currentModelbox = new THREE.Box3().setFromObject(cloneModel);
          } else {
            currentModelbox = new THREE.Box3().setFromObject(relModel);
          }
          // 计算宽度和高度
          const widthA = nationModelBox.max.x - nationModelBox.min.x;
          const heightA = nationModelBox.max.z - nationModelBox.min.z;
          const widthB = currentModelbox.max.x - currentModelbox.min.x;
          const heightB = currentModelbox.max.z - currentModelbox.min.z;
          // 计算宽度和高度的比例
          const widthRatio = widthA / widthB;
          const heightRatio = heightA / heightB;
          // 当前模型与全国模型大小的缩放值
          const scale = (widthRatio + heightRatio) / 2;
          // 应用缩放值到切换后的模型上去
          currentShowModel.value.scale.set(scale, scale, 1);
          // 处理地图数据,绘制模型
          handleMapData(data, scale, resolve);
          // 这时候计算省份模型,得出放大后的省份模型的中心点,并将其位置归于原点
          const scaledBox = new THREE.Box3().setFromObject(currentShowModel.value);
          const center = new THREE.Vector3();
          // 获取放大后模型的中心点
          scaledBox.getCenter(center);
          // 将模型的位置调整,使缩放后的中心位于原点
          currentShowModel.value.position.sub(center);
          // 高度不增加
          currentShowModel.value.position.y += center.y;
          // 显示地图状态切换之后的模型
          currentShowModel.value.traverse((item) => {
            item.visible = true;
          });
          glowArr = [...cylinderGlowArr];
          // 触发右侧按钮点击事件
          rightButItem.map((item) => {
            rightButClick(null, item);
          });
        });
      });
  }

  // 右键双击返回地图上一层
  if (type === 'right') {
    // 全国地图是固定的,省模型、市模型需要清除掉之前的子对象
    if (mapStatus.value !== '国') {
      currentShowModel.value.traverse((item) => {
        // 弹框、标牌等这些为CSS2D和CSS3D对象,要清除其在页面中的dom元素
        if (item.element) {
          if (item.element.parentNode) item.element.parentNode.removeChild(item.element);
        }
        if (item.geometry && item.material) {
          // 从内存中销毁几何体资源
          item.geometry.dispose();
          // 从内存中销毁材质资源
          if (item.material.length) {
            item.material[0].dispose();
            item.material[1].dispose();
          } else {
            item.material.dispose();
          }
        }
      });
      currentShowModel.value.children = [];
    }
    // 地图状态切换,市=>省=>国
    mapStatusSwitch('right');
    if (mapStatus.value === '国') rightButton.value.style.display = 'block';
    // 地图状态转换完成后显示对应的地图
    currentShowModel.value.traverse((item) => {
      item.visible = true;
    });
    // 触发右侧按钮点击事件
    rightButItem.map((item) => {
      rightButClick(null, item);
    });
  }
}

// 处理地图数据,绘制模型
function handleMapData(data, scale, resolve) {
  // 全部信息
  const features = data.features;
  features.map((feature) => {
    const name = feature.properties.name;
    // 海南省三沙市范围太广不方便展示所以跳过
    if (name === '三沙市') {
      return;
    }
    // 模型
    const model = new THREE.Object3D();
    model.name = name;
    model.userData.animationActive = false; // 新增属性来跟踪动画状态
    model.userData.animationTimer = null; // 用于存储定时器的引用
    model.userData.adcode = feature.properties.adcode; // 用于存储定时器的引用
    currentShowModel.value.add(model);
    // 模型中心坐标
    const pos = feature.properties.centroid ? feature.properties.centroid : feature.properties.center;
    // 获取地图名称dom
    const nameDom = document.getElementById('provinceName').cloneNode();
    // 设置dom文本
    nameDom.innerHTML = name;
    // 转为CSS3D对象
    const css3DObject = new CSS3DObject(nameDom);
    css3DObject.rotateX(Math.PI);
    css3DObject.name = '名称对象';
    // 这里转换完成后将元素pointerEvents属性设置为none,防止阻碍相机旋转缩放平移等操作
    nameDom.style.pointerEvents = 'none';
    // 设置名称对象在模型中心位置
    const [x, y] = projection(pos);
    css3DObject.position.set(x, -y, 0);
    css3DObject.rotateX(Math.PI);
    // 缩放一定大小
    css3DObject.scale.set(0.2 / scale, 0.2 / scale, 0.2 / scale);

    const coordinates = feature.geometry.coordinates;
    // 绘制模型和边界线
    if (feature.geometry.type === 'MultiPolygon') {
      coordinates.forEach((coordinate) => {
        coordinate.forEach((rows) => {
          // 城市模型
          const mesh = darwMapModel(rows);
          mesh.rotateX(Math.PI);
          model.add(mesh);
          // 边线
          const line = lineDraw(rows);
          line.name = '边线';
          line.position.z += 0.15;
          model.add(line, css3DObject);
        });
      });
    }
    // 绘制模型和边界线
    if (feature.geometry.type === 'Polygon') {
      coordinates.forEach((coordinate) => {
        // 选中省份模型的材质,将继续应用到地级市模型上
        // 城市模型
        const mesh = darwMapModel(coordinate);
        mesh.rotateX(Math.PI);
        model.add(mesh);
        // 边线
        const line = lineDraw(coordinate);
        line.position.z += 0.15;
        line.name = '边线';
        model.add(line, css3DObject);
      });
    }

    // 转换成平面坐标
    const center = projection(pos);
    center[1] = -center[1];
    // 储存中心位置
    model.userData.center = center;
    // 存储地图数据
    model.userData.mapData = feature.geometry;
    // 创建地图标牌
    const mapTag = createMapTag(
      {
        name: name,
        x: center[0],
        y: center[1],
      },
      Math.random() * 30 + 70
    );
    // 创建光柱
    const cylinder = createCylinder(
      {
        name: name,
        x: center[0],
        y: center[1],
      },
      Math.random() * 30000 + 70000,
      scale
    );
    model.add(mapTag, cylinder);
  });
  resolve();
}

// 绘制地图模型
function darwMapModel(polygon) {
  // 创建形状
  const shape = new THREE.Shape();
  // 遍历坐标数组,绘制形状
  polygon.forEach((row, i) => {
    // 坐标点转换
    const [x, y] = projection(row);
    if (i === 0) {
      shape.moveTo(x, y);
    }
    shape.lineTo(x, y);
  });
  // 将形状进行拉伸
  const geometry = new THREE.ExtrudeGeometry(shape, {
    depth: 10,
    bevelEnabled: true,
    bevelSegments: 10,
    bevelThickness: 0.1,
  });
  // const topMaterial = materialArr[0].clone();
  // const sideMaterial = materialArr[1];
  const mesh = new THREE.Mesh(geometry, [topMaterial, sideMaterial]);
  return mesh;
}

// 绘制边界线
function lineDraw(polygon) {
  const lineGeometry = new THREE.BufferGeometry();
  const pointsArray = new Array();
  polygon.forEach((row) => {
    const [x, y] = projection(row);
    // 创建三维点
    pointsArray.push(new THREE.Vector3(x, -y, 0));
  });
  // 放入多个点
  lineGeometry.setFromPoints(pointsArray);
  const lineMaterial = new THREE.LineBasicMaterial({
    color: '#00ffff',
    // color: "#00C5CD",
  });
  return new THREE.Line(lineGeometry, lineMaterial);
}
</script>
<style lang="less">
body,
html {
  font-size: 0.8vw;
}

/* 当视口宽度小于 600 像素时,设置最小字体大小 */
@media (max-width: 1400px) {
  #mapTag {
    font-size: 12px !important;
    width: 80px !important;
    height: 30px !important;
  }
}

#chinaMap {
  width: 100%;
  height: 100%;
  position: absolute;
  overflow: hidden;
}

#threejs {
  width: 100%;
  height: 100%;
}

.rightButton {
  position: absolute;
  right: 1vw;
  bottom: 40vh;
  width: 4vw;

  .common {
    width: 100%;
    height: 3vh;
    border: 1px solid #00ffff;
    display: flex;
    justify-content: center;
    align-items: center;
    margin: 1.2vh 0;
    color: #fafafa;
    opacity: 0.5;
    font-size: 0.7vw;
    cursor: pointer;
    transition: 1s;
  }

  .selected {
    opacity: 1 !important;
    transition: 1s;
  }
}

#provinceName {
  pointer-events: none;
  position: absolute;
  left: 0;
  top: 0;
  color: #8ee5ee;
  padding: 10px;
  width: 200px;
  height: 20px;
  line-height: 20px;
  text-align: center;
  font-size: 13px;
}

#popup {
  z-index: 999;
  position: absolute;
  left: 0px;
  top: 0px;
  width: 41.66vw;
  height: 26.59vh;
  display: flex;

  .popup_line {
    margin-top: 4%;
    width: 24%;
    height: 26%;
    background: url('../../public/popup_line.png') no-repeat;
    background-size: 100% 100%;
  }

  .popup_Main {
    width: 35%;
    height: 80%;

    .popupMain_top {
      width: 100%;
      height: 10%;
      background: url('../../public/popupMain_head.png') no-repeat;
      background-size: 100% 100%;
    }

    .popupMain_footer {
      width: 100%;
      height: 10%;
      background: url('../../public/popupMain_footer.png') no-repeat;
      background-size: 100% 100%;
    }

    .popup_content {
      color: #fafafa;
      // background: rgba(47, 53, 121, 0.9);
      background-image: linear-gradient(to bottom, rgba(15, 36, 77, 1), rgba(8, 124, 190, 1));
      border-radius: 10px;
      width: 100%;
      height: 70%;
      padding: 5% 0%;

      .popup_head {
        width: 100%;
        height: 12%;
        margin-bottom: 2%;
        display: flex;
        align-items: center;

        .popup_title {
          color: #8ee5ee;
          font-size: 1vw;
          letter-spacing: 5px;
          width: 88%;
          height: 100%;
          display: flex;
          align-items: center;

          .title_icon {
            width: 0.33vw;
            height: 100%;
            background: #2586ff;
            margin-right: 10%;
          }
        }

        .close {
          cursor: pointer;
          pointer-events: auto;
          width: 1.5vw;
          height: 1.5vw;
          background: url('../../public/close.png') no-repeat;
          background-size: 100% 100%;
        }
      }

      .popup_item {
        display: flex;
        align-items: center;
        width: 85%;
        padding-left: 5%;
        height: 18%;
        // background: rgb(160, 196, 221);
        border-radius: 10px;
        margin: 2.5% 0%;
        margin-left: 10%;

        div {
          line-height: 100%;
          margin-right: 10%;
        }

        .item_value {
          font-size: 0.9vw;
          color: #00ffff;
          font-weight: 600;
          letter-spacing: 2px;
        }
      }
    }
  }
}

#cylinderValue {
  position: absolute;
  top: 0;
  left: 0;
  color: #bbffff;
}

#mapTag {
  z-index: 997;
  position: absolute;
  top: 0;
  left: 0;
  font-size: 0.6vw;
  width: 4.2vw;
  height: 4.7vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;

  .content {
    width: 100%;
    height: calc(100% - 1vw);
    // background: #0e1937;
    background: #0e2346;
    border: 1px solid #6298a9;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fafafa;

    #mapTag_value {
      color: #ffd700;
    }
  }

  .content::before {
    content: '';
    width: 100%;
    height: calc(100% - 1vw);
    position: absolute;
    background: linear-gradient(to top, #26aad1, #26aad1) left top no-repeat,
      //上左
      linear-gradient(to right, #26aad1, #26aad1) left top no-repeat,
      linear-gradient(to top, #26aad1, #26aad1) right bottom no-repeat,
      //下右
      linear-gradient(to left, #26aad1, #26aad1) right bottom no-repeat; //右下
    background-size: 2px 10px, 16px 2px, 2px 10px, 16px 2px;
    pointer-events: none;
  }

  .arrow {
    background: url('../../public/arrow.png') no-repeat;
    background-size: 100% 100%;
    width: 1vw;
    height: 1vw;
  }
}
</style>

 

posted @ 2024-11-02 19:15  JackGIS  阅读(62)  评论(0编辑  收藏  举报