vue3中实现3D地图——three.js

news2024/11/20 14:38:41

在这里插入图片描述

需求点

  • 地图区域大小随着父盒子大小变动,窗口缩放自动适配
  • 每个区域显示不同颜色和高度,描边
  • 每个区域显示名字label和icon
  • 点击区域改变其透明度,并且弹窗显示信息窗口
  • 点击点也可以
  • 可以自由放大缩小,360度旋转

在这里插入图片描述

npm install d3@^7.8.4
npm install lil-gui@^0.18.1
npm install three@^0.152.2

代码

<template>
  <div style="width: 100%; height: 100vh; position: relative">
    <div id="map" style="width: 100%; height: 100%"></div>
    <!-- 点击市或者点显示信息窗口 -->
    <div v-if="city" class="infoPop" :style="`left:${left}px;top:${top}px`">
      我是{{ city }}
    </div>
  </div>
</template>

<script setup>
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import {
  CSS2DRenderer,
  CSS2DObject,
} from "three/examples/jsm/renderers/CSS2DRenderer.js";
import { ref, onMounted, onUnmounted } from "vue";
import * as d3 from "d3";

const mapWidth = ref(0);
const mapHeight = ref(0);
const mapElement = ref(null);
const left = ref(0);
const top = ref(0);
const city = ref("");

onMounted(() => {
  // 获取map地图容器的宽高,监听窗口变化更新宽高
  mapElement.value = document.getElementById("map"); // 获取DOM元素引用
  mapWidth.value = mapElement.value.clientWidth;
  mapHeight.value = mapElement.value.clientHeight;
  console.log(mapWidth.value, mapHeight.value);

  const scene = new THREE.Scene();

  // const axesHelper = new THREE.AxesHelper(5);
  // scene.add(axesHelper);
  const ambientLight = new THREE.AmbientLight(0xd4e7fd, 4);
  scene.add(ambientLight);
  const directionalLight = new THREE.DirectionalLight(0xe8eaeb, 0.2);
  directionalLight.position.set(0, 10, 5);
  const directionalLight2 = directionalLight.clone();
  directionalLight2.position.set(0, 10, -5);
  const directionalLight3 = directionalLight.clone();
  directionalLight3.position.set(5, 10, 0);
  const directionalLight4 = directionalLight.clone();
  directionalLight4.position.set(-5, 10, 0);
  scene.add(directionalLight);
  scene.add(directionalLight2);
  scene.add(directionalLight3);
  scene.add(directionalLight4);

  const camera = new THREE.PerspectiveCamera(
    75,
    mapWidth.value / mapHeight.value,
    0.1,
    1000
  );
  camera.position.y = 8;
  camera.position.z = 8;

  const labelRenderer = new CSS2DRenderer();
  labelRenderer.domElement.style.position = "absolute";
  labelRenderer.domElement.style.top = "0px";
  labelRenderer.domElement.style.pointerEvents = "none";
  labelRenderer.setSize(mapWidth.value, mapHeight.value);
  document.getElementById("map").appendChild(labelRenderer.domElement);

  const renderer = new THREE.WebGLRenderer({ alpha: true });

  renderer.setSize(mapWidth.value, mapHeight.value);
  document.getElementById("map").appendChild(renderer.domElement);
  const controls = new OrbitControls(camera, renderer.domElement);
  controls.update();
  const animate = () => {
    requestAnimationFrame(animate);
    controls.update();
    renderer.render(scene, camera);
    labelRenderer.render(scene, camera);
  };
  animate();
  window.addEventListener("resize", () => {
    mapWidth.value = mapElement.value.clientWidth;
    mapHeight.value = mapElement.value.clientHeight;
    camera.aspect = mapWidth.value / mapHeight.value;
    camera.updateProjectionMatrix();
    renderer.setSize(mapWidth.value, mapHeight.value);
    labelRenderer.setSize(mapWidth.value, mapHeight.value);
  });

  const url = "https://geo.datav.aliyun.com/areas_v3/bound/330000_full.json";
  fetch(url)
    .then((res) => res.json())
    .then((data) => {
      const map = createMap(data);
      scene.add(map);

      let intersect = null;
      // 鼠标点击事件
      window.addEventListener("click", (event) => {
        const mouse = new THREE.Vector2();
        mouse.x = (event.clientX / mapWidth.value) * 2 - 1;
        mouse.y = -(event.clientY / mapHeight.value) * 2 + 1;
        const raycaster = new THREE.Raycaster();
        raycaster.setFromCamera(mouse, camera);
        const intersects = raycaster
          .intersectObjects(map.children)
          .filter((item) => item.object.type !== "Line");
        if (intersects.length > 0) {
          // 点击市
          if (intersects[0].object.type === "Mesh") {
            // 打印鼠标点击的省市名称、adcode,显示信息窗口
            console.log(
              intersects[0].object.parent.name,
              intersects[0].object.parent.adcode
            );
            city.value = intersects[0].object.parent.name;
            left.value = event.clientX + 20;
            top.value = event.clientY + 20;

            if (intersect) isAplha(intersect, 1);
            intersect = intersects[0].object.parent;
            isAplha(intersect, 0.4);
          }
          // 点击icon
          if (intersects[0].object.type === "Sprite") {
            console.log(intersects[0].object);
          }
        } else {
          if (intersect) isAplha(intersect, 1);
        }
        function isAplha(intersect, opacity) {
          intersect.children.forEach((item) => {
            if (item.type === "Mesh") {
              item.material.opacity = opacity;
            }
          });
        }
      });
    });
});
onUnmounted(() => {
  window.removeEventListener("resize", resizeRenderer); // 移除窗口大小变化监听器
});
// 矫正坐标
const offsetXY = d3.geoMercator();

// 根据省市的json数据创建地图
const createMap = (data) => {
  console.log(data, 11111);
  const map = new THREE.Object3D();
  const center = data.features[0].properties.centroid;
  offsetXY.center(center).translate([0, 0]);
  data.features.forEach((feature) => {
    const unit = new THREE.Object3D();
    const { centroid, center, name, adcode } = feature.properties;
    const { coordinates, type } = feature.geometry;
    const point = centroid || center || [0, 0];

    // 每个市随机颜色,随机深度
    const color = new THREE.Color(`hsl(
      ${233},
      ${Math.random() * 30 + 55}%,
      ${Math.random() * 30 + 55}%)`).getHex();
    const depth = Math.random() * 0.3 + 0.3;

    // 绘制每个市的名称和图标
    const label = createLabel(name, point, depth);
    const icon = createIcon(center, depth);

    coordinates.forEach((coordinate) => {
      if (type === "MultiPolygon") coordinate.forEach((item) => fn(item));
      if (type === "Polygon") fn(coordinate);

      function fn(coordinate) {
        // 添加自定义属性,点击的时候可以打印出来
        unit.name = name;
        unit.adcode = adcode;
        // 绘制每个市的区域(传入颜色和深度)
        const mesh = createMesh(coordinate, color, depth);
        // 绘制每个市的边界
        const line = createLine(coordinate, depth);
        unit.add(mesh, ...line);
      }
    });
    // 在地图上添加标记 位置、名字、图标
    map.add(unit, label, icon);
    // 设置地图中心
    setCenter(map);
  });

  // ----添加任意点标记----
  const icon1 = createIcon([120.057576, 29.697459], 1.11);
  map.add(icon1);

  return map;
};
// 绘制每个市的区域
const createMesh = (data, color, depth, name) => {
  const shape = new THREE.Shape();
  data.forEach((item, idx) => {
    const [x, y] = offsetXY(item);

    if (idx === 0) shape.moveTo(x, -y);
    else shape.lineTo(x, -y);
  });

  const extrudeSettings = {
    depth: depth,
    bevelEnabled: false,
  };
  const materialSettings = {
    color: color,
    emissive: 0x000000,
    roughness: 0.45,
    metalness: 0.8,
    transparent: true,
    side: THREE.DoubleSide,
  };
  const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
  const material = new THREE.MeshStandardMaterial(materialSettings);
  const mesh = new THREE.Mesh(geometry, material);

  return mesh;
};
// 绘制每个市的边界
const createLine = (data, depth) => {
  const points = [];
  data.forEach((item) => {
    const [x, y] = offsetXY(item);
    points.push(new THREE.Vector3(x, -y, 0));
  });
  const lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
  const uplineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });
  const downlineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });

  const upLine = new THREE.Line(lineGeometry, uplineMaterial);
  const downLine = new THREE.Line(lineGeometry, downlineMaterial);
  downLine.position.z = -0.0001;
  upLine.position.z = depth + 0.0001;
  return [upLine, downLine];
};
// 绘制每个市的名称
const createLabel = (name, point, depth) => {
  const div = document.createElement("div");
  div.style.color = "#fff";
  div.style.fontSize = "12px";
  div.style.textShadow = "1px 1px 2px #047cd6";
  div.textContent = name;
  const label = new CSS2DObject(div);
  label.scale.set(0.01, 0.01, 0.01);
  const [x, y] = offsetXY(point);
  label.position.set(x, -y, depth);
  return label;
};
// 绘制每个市的图标
const createIcon = (point, depth) => {
  const url = new URL("../assets/icon.png", import.meta.url).href;
  const map = new THREE.TextureLoader().load(url);
  const material = new THREE.SpriteMaterial({
    map: map,
    transparent: true,
  });
  const sprite = new THREE.Sprite(material);
  const [x, y] = offsetXY(point);
  sprite.scale.set(0.3, 0.3, 0.3);

  sprite.position.set(x, -y, depth + 0.2);
  sprite.renderOrder = 1;

  return sprite;
};
// 设置地图中心
const setCenter = (map) => {
  map.rotation.x = -Math.PI / 2;
  const box = new THREE.Box3().setFromObject(map);
  const center = box.getCenter(new THREE.Vector3());

  const offset = [0, 0];
  map.position.x = map.position.x - center.x - offset[0];
  map.position.z = map.position.z - center.z - offset[1];
};
</script>

<style>
* {
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}
#map {
  background-color: #d4e7fd;
}
.infoPop {
  position: absolute;
  left: 0;
  top: 0;
  background: #fff;
  border: 1px solid #03c3fd;
  border-radius: 10px;
  color: #333;
  padding: 20px;
  z-index: 9999;
}
</style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1838059.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

六西格玛培训新选择,老字号品质有保障!

在追求企业卓越与完美的道路上&#xff0c;六西格玛管理无疑是一个被广泛认可与采纳的方法论。六西格玛不仅仅是一种管理策略&#xff0c;更是一种文化和哲学&#xff0c;它强调通过数据驱动和持续改进来减少流程中的缺陷&#xff0c;提升客户满意度&#xff0c;并最终实现企业…

两轮车换电也卷得不行?铁塔换电、这锂换电浴血奋战

配图来自Canva可画 当两轮电动车智能化和高端化竞争&#xff0c;无法再有更多突破&#xff0c;卷无可卷时&#xff0c;换电这个具有巨大潜力的新兴领域&#xff0c;引起了市场的关注。 早在多年前&#xff0c;哈啰、美团、雅迪等两轮电动车品牌就推出自己的换电品牌。据不完全…

【Java毕业设计】基于JavaWeb的物流信息网系统

文章目录 摘 要Abstract目录1 绪论1.1 课题背景和意义1.2 国内外研究现状1.2.1 国外研究现状1.2.2 国内研究现状1.3 课题主要内容 2 开发相关技术介绍2.1 系统开发环境2.2 系统开发技术2.2.1 Spring Boot框架2.2.2 MySQL数据库 3 系统规划3.1 初步调查分析3.2 可行性分析3.2.1 …

一文彻底搞懂 Transformer(图解+手撕)

Transformers 亮相以来彻底改变了深度学习模型。 今天&#xff0c;我们来揭示 Transformers 背后的核心概念&#xff1a;注意力机制、编码器-解码器架构、多头注意力等等。通过 Python 代码片段&#xff0c;让你深入了解其原理。 一、理解注意力机制 注意力机制是神经网络中…

Java Stream流应用

Stream流的核心方法 Stream流的方法主要包含如图的几种 提供部分应用场景做个思考&#xff1a; (1)从员工集合中筛选出salary大于8000的员工&#xff0c;并放置到新的集合里。 (2)统计员工的最高薪资、平均薪资、薪资之和。 (3)将员工按薪资从高到低排序&#xff0c;同样薪资…

编曲旋律怎么配和弦 旋律怎么编曲 编曲和弦怎么写

在音乐的创作中&#xff0c;编曲旋律与和弦是两个重要的元素&#xff0c;它们相辅相成&#xff0c;共同构建出动听的音乐作品。而编曲旋律与和弦之间的搭配&#xff0c;就需要合理的安排和设计&#xff0c;才能达到最佳的效果。接下来给大家介绍编曲旋律怎么配和弦&#xff0c;…

深入理解计算机系统 CSAPP 家庭作业6.45

CS:APP3e, Bryant and OHallaron 可以参考这里

有什么文档翻译免费?无基础处理外语资料

在日常工作中&#xff0c;我们经常需要处理各种语言的文档&#xff0c;从英文合同到法文报告&#xff0c;它们无处不在。 迅速掌握这些外文资料的内容&#xff0c;对于提升我们的工作效率至关重要。幸运的是&#xff0c;借助现代科技&#xff0c;我们可以利用文档翻译工具&…

C语言入门系列:判断和循环常踩的5个坑

文章目录 1. if代码块不带大括号问题描述示例与分析解决办法 2. if条件和大括号之间加了一个分号问题描述示例与分析解决办法 3. 使用号判断相等问题描述示例与分析解决办法 4. while循环的无限循环问题描述示例与分析解决办法 5. for循环中的off-by-one错误问题描述示例与分析…

HarmonyOS开发 :Router 和 NavPatchStatck 如何实现跳转(传参)及页面回调

路由的选择 HarmonyOS提供两种路由实现的方式&#xff0c;分别是 Router 和 NavPatchStack。两者使用场景和特效各有优劣。 组件适用场景特点备注Router模块间与模块内页面切换通过每个页面的url实现模块间解耦NavPathStack模块内页面切换通过组件级路由统一路由管理 什么时…

凌凯科技冲刺上市:2023年业绩反弹,靠关联交易助推业务发展?

近日&#xff0c;上海凌凯科技股份有限公司&#xff08;下称“凌凯科技”&#xff09;向港交所递交上市申请&#xff0c;华泰国际担任其独家保荐人。 透过招股书不难看出&#xff0c;在化学合成一体化这个虹吸效应显著的细分赛道中&#xff0c;凌凯科技拥有头部玩家的先发优势…

freemarker导出doc文档多个图片处理

POI freemarker处理多图片插入到doc文档 文章目录 POI前言一、doc模板转换成xml文件格式&#xff1f;二、修改xml文件并转为ftl文件1.集合内容2.xml修改集合处理&#xff08;1&#xff09;头部加入图片的循环&#xff08;2&#xff09;需要循环的数据集合处理&#xff08;3&am…

计算机SCI期刊,中科院2区TOP,收稿范围广泛!

一、期刊名称 IEEE Transactions on Automation Science and Engineering 二、期刊简介概况 期刊类型&#xff1a;SCI 学科领域&#xff1a;计算机科学 影响因子&#xff1a;5.6 中科院分区&#xff1a;2区top 三、期刊征稿范围 IEEE Transactions on Automation Science…

SAP ABAP开发:如何读取物料主数据中的长文本?

在SAP ERP系统中&#xff0c;物料的基本描述可存储40个字符&#xff0c;见下图&#xff1a; 但长文本信息如何从系统中读取呢&#xff1f; 在SAP ABAP开发中&#xff0c;读取物料主数据&#xff08;Material Master Data&#xff09;中的基本视图&#xff08;Basic View&#…

Redis变慢了?之二

Redis变慢了&#xff1f;之二 Redis变慢了规律性变慢Redis几种过期策略的区别&#xff1f;定时过期惰性过期定期过期优化方案 实例内存达上限内存淘汰策略 写在最后 Redis变慢了 Redis变慢上一篇文章地址&#xff1a;Redis变慢了&#xff1f;这篇文章继续Redis变慢情况的分析。…

PyQt5.QtWidgets常用函数及说明

目录 PyQt5.QtWidgets简介常用函数设置窗口标题和固定大小创建垂直布局创建进度条 PyQt5.QtWidgets简介 PyQt5.QtWidgets 是 PyQt5 库中的一个模块&#xff0c;它包含了用于创建图形用户界面&#xff08;GUI&#xff09;的各种小部件&#xff08;widgets&#xff09;。这些小部…

【ARM】如何通过Keil MDK查看芯片的硬件信息

【更多软件使用问题请点击亿道电子官方网站】 1、文档目标&#xff1a; 解决在开发过程中对于开发项目所使用的的芯片的参数查看的问题 2、问题场景&#xff1a; 在项目开发过程中&#xff0c;经常需要对于芯片的时钟、寄存器或者一些硬件参数需要进行确认。大多数情况下是需…

【前端取不到cookie的的原因】http-only

某条cookie有http-only属性时&#xff0c;下面两种方法都取不到&#xff0c;还是改需求吧&#xff0c;别取了 1、 npm install js-cookie --save import Cookies from js-cookie let cookieValue Cookies.get(name)2、document.cookie

STM32单片机-通信协议(下)

STM32单片机-通信协议(下&#xff09; 一、通信协议介绍二、USART(通用同步/异步收发器)2.1 USART框图和基本结构2.2 串口发送2.2.1 Printf函数移植2.2.2 串口发送汉字 2.3 串口接收2.3.1 串口接收查询2.3.2 串口接收中断 2.4 USART串口数据包2.4.1 数据包格式2.4.2 数据包接收…

可平滑替代传统FTP的国产FTP方案,了解一下

企业在处理数据传输时&#xff0c;效率和安全性是关键。尽管传统FTP曾被广泛采用&#xff0c;然而&#xff0c;随着企业业务需求的增长&#xff0c;传统FTP在传输速度、安全性、稳定性以及可控性方面的不足逐渐显现。许多企业正在寻找更为高效、安全且用户体验更好的的国产FTP方…