three.js+vue3三維地圖下鑽地圖,實現下鑽全國-》省份-》城市-》區縣

JackGIS發表於2024-11-02

案例效果截圖:

具體場景和功能,詳見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>

相關文章