范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

three。js实现3D地图下钻

  地图下钻是前端开发中常见的开发需求。通常会使用高德、百度等第三方地图实现,不过这些都不是3d的。echarts倒是提供了map3D,以及常用的点位、飞线等功能,就是有一些小bug[泪奔],而且如果领导比较可爱,提一些奇奇怪怪的需求,可能就不好搞了……
  这篇文章我会用three.js实现一个geojson下钻地图。
  地图预览一、搭建环境
  我这里用parcel搭建一个简易的开发环境,安装依赖如下:{   "name": "three",   "version": "1.0.0",   "description": "",   "main": "index.js",   "scripts": {     "dev": "parcel src/index.html",     "build": "parcel build src/index.html"   },   "author": "",   "license": "ISC",   "devDependencies": {     "parcel-bundler": "^1.12.5"   },   "dependencies": {     "d3": "^7.6.1",     "d3-geo": "^3.0.1",     "three": "^0.142.0"   } }二、创建场景、相机、渲染器以及地图import * as THREE from "three"  class Map3D {   constructor() {     this.scene = undefined  // 场景     this.camera = undefined // 相机     this.renderer = undefined // 渲染器      this.init()   }   init() {     // 创建场景     this.scene = new THREE.Scene()      // 创建相机     this.setCamera()      // 创建渲染器     this.setRender()      // 渲染函数     this.render()    }   /**    * 创建相机    */   setCamera() {     // PerspectiveCamera(角度,长宽比,近端面,远端面) —— 透视相机     this.camera = new THREE.PerspectiveCamera(       75,       window.innerWidth / window.innerHeight,       0.1,       1000     )     // 设置相机位置     this.camera.position.set(0, 0, 120)     // 把相机添加到场景中     this.scene.add(this.camera)   }   /**    * 创建渲染器    */   setRender() {     this.renderer = new THREE.WebGLRenderer()     // 渲染器尺寸     this.renderer.setSize(window.innerWidth, window.innerHeight)     //设置背景颜色     this.renderer.setClearColor(0x000000)     // 将渲染器追加到dom中     document.body.appendChild(this.renderer.domElement)   }   render() {     this.renderer.render(this.scene, this.camera)     requestAnimationFrame(this.render.bind(this))   } }  const map = new Map3D()
  场景、相机、渲染器是threejs中必不可少的要素。以上代码运行起来后可以看到屏幕一片黑,审查元素是一个canvas占据了窗口。
  啥也没有
  接下来需要geojson数据了,阿里的datav免费提供区级以上的数据:https://datav.aliyun.com/portal/school/atlas/area_selectorclass Map3D {   // 省略代码      // 以下为新增代码   init() {          ......        	this.loadData()   }   getGeoJson (adcode = "100000") {     return fetch(`https://geo.datav.aliyun.com/areas_v3/bound/${adcode}_full.json`)     .then(res => res.json())   }   async loadData(adcode) {     this.geojson = await this.getGeoJson(adcode)     console.log(this.geojson)   } }  const map = new Map3D()
  得到的json大概是下图这样的数据格式:
  geojson
  然后,我们初始化一个地图 当然,咱们拿到的json数据中的所有坐标都是经纬度坐标,是不能直接在我们的threejs项目中使用的。需要 "墨卡托投影转换  "把经纬度转换成画布中的坐标。在这里,我们使用现成的工具——d3中的墨卡托投影转换工具import * as d3 from "d3-geo" class Map3D {      ......      async loadData(adcode) {     // 获取geojson数据     this.geojson = await this.getGeoJson(adcode)          // 墨卡托投影转换。将中心点设置成经纬度为 104.0, 37.5 的地点,且不平移     this.projection = d3     	.geoMercator()       .center([104.0, 37.5])       .translate([0, 0])        } }
  接着就可以创建地图了。
  创建地图的思路:以中国地图为例,创建一个Object3D对象,作为整个中国地图。再创建N个Object3D子对象,每个子对象都是一个省份,再将这些子对象add到中国地图这个父Object3D对象上。
  地图结构
  创建地图后的完整代码:import * as THREE from "three" import * as d3 from "d3-geo"  const MATERIAL_COLOR1 = "#2887ee"; const MATERIAL_COLOR2 = "#2887d9";  class Map3D {   constructor() {     this.scene = undefined  // 场景     this.camera = undefined // 相机     this.renderer = undefined // 渲染器     this.geojson = undefined // 地图json数据      this.init()   }   init() {     // 创建场景     this.scene = new THREE.Scene()      // 创建相机     this.setCamera()      // 创建渲染器     this.setRender()      // 渲染函数     this.render()      // 加载数据     this.loadData()    }   /**    * 创建相机    */   setCamera() {     // PerspectiveCamera(角度,长宽比,近端面,远端面) —— 透视相机     this.camera = new THREE.PerspectiveCamera(       75,       window.innerWidth / window.innerHeight,       0.1,       1000     )     // 设置相机位置     this.camera.position.set(0, 0, 120)     // 把相机添加到场景中     this.scene.add(this.camera)   }   /**    * 创建渲染器    */   setRender() {     this.renderer = new THREE.WebGLRenderer()     // 渲染器尺寸     this.renderer.setSize(window.innerWidth, window.innerHeight)     //设置背景颜色     this.renderer.setClearColor(0x000000)     // 将渲染器追加到dom中     document.body.appendChild(this.renderer.domElement)   }   render() {     this.renderer.render(this.scene, this.camera)     requestAnimationFrame(this.render.bind(this))   }   getGeoJson (adcode = "100000") {     return fetch(`https://geo.datav.aliyun.com/areas_v3/bound/${adcode}_full.json`)     .then(res => res.json())   }   async loadData(adcode) {     // 获取geojson数据     this.geojson = await this.getGeoJson(adcode)          // 创建墨卡托投影     this.projection = d3       .geoMercator()       .center([104.0, 37.5])       .translate([0, 0])          // Object3D是Three.js中大部分对象的基类,提供了一系列的属性和方法来对三维空间中的物体进行操纵。     // 初始化一个地图     this.map = new THREE.Object3D();     this.geojson.features.forEach(elem => {       const area = new THREE.Object3D()       // 坐标系数组(为什么是数组,因为有地区不止一个几何体,比如河北被北京分开了,比如舟山群岛)       const coordinates = elem.geometry.coordinates       const type = elem.geometry.type        // 定义一个画几何体的方法       const drawPolygon = (polygon) => {         // Shape(形状)。使用路径以及可选的孔洞来定义一个二维形状平面。 它可以和ExtrudeGeometry、ShapeGeometry一起使用,获取点,或者获取三角面。         const shape = new THREE.Shape()         // 存放的点位,最后需要用THREE.Line将点位构成一条线,也就是地图上区域间的边界线         // 为什么两个数组,因为需要三维地图的两面都画线,且它们的z坐标不同         let points1 = [];         let points2 = [];          for (let i = 0; i < polygon.length; i++) {           // 将经纬度通过墨卡托投影转换成threejs中的坐标           const [x, y] = this.projection(polygon[i]);           // 画二维形状           if (i === 0) {             shape.moveTo(x, -y);           }           shape.lineTo(x, -y);            points1.push(new THREE.Vector3(x, -y, 10));           points2.push(new THREE.Vector3(x, -y, 0));         }          /**          * ExtrudeGeometry (挤压缓冲几何体)          * 文档链接:https://threejs.org/docs/index.html?q=ExtrudeGeometry#api/zh/geometries/ExtrudeGeometry          */         const geometry = new THREE.ExtrudeGeometry(shape, {           depth: 10,           bevelEnabled: false,         });         /**          * 基础材质          */         // 正反两面的材质         const material1 = new THREE.MeshBasicMaterial({           color: MATERIAL_COLOR1,         });         // 侧边材质         const material2 = new THREE.MeshBasicMaterial({           color: MATERIAL_COLOR2,         });         // 生成一个几何物体(如果是中国地图,那么每一个mesh就是一个省份几何体)         const mesh = new THREE.Mesh(geometry, [material1, material2]);         area.add(mesh);          /**          * 画线          * link: https://threejs.org/docs/index.html?q=Line#api/zh/objects/Line          */         const lineGeometry1 = new THREE.BufferGeometry().setFromPoints(points1);         const lineGeometry2 = new THREE.BufferGeometry().setFromPoints(points2);         const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });         const line1 = new THREE.Line(lineGeometry1, lineMaterial);         const line2 = new THREE.Line(lineGeometry2, lineMaterial);         area.add(line1);         area.add(line2);       }        // type可能是MultiPolygon 也可能是Polygon       if (type === "MultiPolygon") {         coordinates.forEach((multiPolygon) => {           multiPolygon.forEach((polygon) => {             drawPolygon(polygon);           });         });       } else {         coordinates.forEach((polygon) => {           drawPolygon(polygon);         });       }        // 把区域添加到地图中       this.map.add(area);      })      // 把地图添加到场景中     this.scene.add(this.map)   } }  const map = new Map3D()
  简单地图
  这时,已经生成一个完整的地图,但是当我们试着去交互时还不能旋转,只需要添加一个控制器// 引入构造器 import { OrbitControls  } from "three/examples/jsm/controls/OrbitControls"  init() {   this.setControls() } setControls() {     this.controls = new OrbitControls(this.camera, this.renderer.domElement)     // 太灵活了,来个阻尼     this.controls.enableDamping = true;     this.controls.dampingFactor = 0.1; }
  controls
  好了,现在就可以想看哪儿就看哪儿了。
  三、当鼠标移入地图时让对应的地区高亮
  Raycaster —— 光线投射Raycaster
  文档链接:https://threejs.org/docs/index.html?q=Raycaster#api/zh/core/Raycaster
  Raycaster用于进行 raycasting(光线投射)。 光线投射用于进行鼠标拾取(在三维空间中计算出鼠标移过了什么物体)。
  这个类有两个方法,
  第一个setFromCamera(coords, camera)方法,它接收两个参数:
  coords —— 在标准化设备坐标中鼠标的二维坐标 —— X分量与Y分量应当在-1到1之间。
  camera —— 射线所来源的摄像机。
  通过 这个方法可以更新射线。
  第二个intersectObjects: 检测所有在射线与这些物体之间,包括或不包括后代的相交部分。返回结果时,相交部分将按距离进行排序,最近的位于第一个)。
  我们可以通过监听鼠标事件,实时更新鼠标的坐标,同时实时在渲染函数中更新射线,然后通过intersectObjects方法查找当前鼠标移过的物体。// 以下是新添加的代码  init() {     // 创建场景     this.scene = new THREE.Scene()      // 创建相机     this.setCamera()      // 创建渲染器     this.setRender()          // 创建控制器     this.setControls()      // 光线投射     this.setRaycaster()          // 加载数据     this.loadData()      // 渲染函数     this.render() } setRaycaster() {     this.raycaster = new THREE.Raycaster();     this.mouse = new THREE.Vector2();     const onMouse = (event) => {       // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)       // threejs的三维坐标系是中间为原点,鼠标事件的获得的坐标是左上角为原点。因此需要在这里转换       this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1       this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1     };     window.addEventListener("mousemove", onMouse, false); } render() {     this.raycaster.setFromCamera(this.mouse, this.camera)      const intersects = this.raycaster.intersectObjects(       this.scene.children,       true     )          // 如果this.lastPick存在,将材质颜色还原     if (this.lastPick) {       this.lastPick.object.material[0].color.set(MATERIAL_COLOR1);       this.lastPick.object.material[1].color.set(MATERIAL_COLOR2);     }     // 置空     this.lastPick = null;     // 查询当前鼠标移动所产生的射线与物体的焦点     // 有两个material的就是我们要找的对象     this.lastPick = intersects.find(       (item) => item.object.material && item.object.material.length === 2     );     // 找到后把颜色换成一个鲜艳的绿色     if (this.lastPick) {       this.lastPick.object.material[0].color.set("aquamarine");       this.lastPick.object.material[1].color.set("aquamarine");     }      this.renderer.render(this.scene, this.camera)     requestAnimationFrame(this.render.bind(this)) }
  高亮四、还差一个tooltip
  引入 CSS2DRenderer 和 CSS2DObject,创建一个2D渲染器,用2D渲染器生成一个tooltip。在此之前,需要在 loadData方法创建area时把地区属性添加到Mesh对象上。确保lastPick对象上能取到地域名称。// 把地区属性存到area对象中 area.properties = elem.properties
  把地区属性存到Mash对象中// 引入CSS2DObject, CSS2DRenderer import { CSS2DObject, CSS2DRenderer } from "three/examples/jsm/renderers/CSS2DRenderer" class Map3D {      setRender() { 		......     // CSS2DRenderer 创建的是html的p元素     // 这里将p设置成绝对定位,盖住canvas画布     this.css2dRenderer = new CSS2DRenderer();     this.css2dRenderer.setSize(window.innerWidth, window.innerHeight);     this.css2dRenderer.domElement.style.position = "absolute";     this.css2dRenderer.domElement.style.top = "0px";     this.css2dRenderer.domElement.style.pointerEvents = "none";     document.body.appendChild(this.css2dRenderer.domElement);   }   render() {     // 省略......     this.showTip()     this.css2dRenderer.render(this.scene, this.camera)     // 省略 ......   }   showTip () {     if (!this.dom) {       this.dom = document.createElement("p");       this.tip = new CSS2DObject(this.dom);     }     if (this.lastPick) {       const { x, y, z } = this.lastPick.point;       const properties = this.lastPick.object.parent.properties;       // label的样式在直接用css写在样式表中       this.dom.className = "label";       this.dom.innerText = properties.name       this.tip.position.set(x + 10, y + 10, z);       this.map && this.map.add(this.tip);     }   }    }
  label样式
  3D中国地图
  此时的完整代码:import * as THREE from "three" import * as d3 from "d3-geo" import { OrbitControls  } from "three/examples/jsm/controls/OrbitControls" import { CSS2DObject, CSS2DRenderer } from "three/examples/jsm/renderers/CSS2DRenderer"  const MATERIAL_COLOR1 = "#2887ee"; const MATERIAL_COLOR2 = "#2887d9";  class Map3D {   constructor() {     this.scene = undefined  // 场景     this.camera = undefined // 相机     this.renderer = undefined // 渲染器     this.css2dRenderer = undefined // html渲染器     this.geojson = undefined // 地图json数据      this.init()   }   init() {     // 创建场景     this.scene = new THREE.Scene()      // 创建相机     this.setCamera()      // 创建渲染器     this.setRender()          // 创建控制器     this.setControls()      // 光线投射     this.setRaycaster()          // 加载数据     this.loadData()      // 渲染函数     this.render()    }   /**    * 创建相机    */   setCamera() {     // PerspectiveCamera(角度,长宽比,近端面,远端面) —— 透视相机     this.camera = new THREE.PerspectiveCamera(       75,       window.innerWidth / window.innerHeight,       0.1,       1000     )     // 设置相机位置     this.camera.position.set(0, 0, 120)     // 把相机添加到场景中     this.scene.add(this.camera)   }   /**    * 创建渲染器    */   setRender() {     this.renderer = new THREE.WebGLRenderer()     // 渲染器尺寸     this.renderer.setSize(window.innerWidth, window.innerHeight)     //设置背景颜色     this.renderer.setClearColor(0x000000)     // 将渲染器追加到dom中     document.body.appendChild(this.renderer.domElement)      // CSS2DRenderer 创建的是html的p元素     // 这里将p设置成绝对定位,盖住canvas画布     this.css2dRenderer = new CSS2DRenderer();     this.css2dRenderer.setSize(window.innerWidth, window.innerHeight);     this.css2dRenderer.domElement.style.position = "absolute";     this.css2dRenderer.domElement.style.top = "0px";     this.css2dRenderer.domElement.style.pointerEvents = "none";     document.body.appendChild(this.css2dRenderer.domElement);   }   setRaycaster() {     this.raycaster = new THREE.Raycaster();     this.mouse = new THREE.Vector2();     const onMouse = (event) => {       // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)       // threejs的三维坐标系是中间为原点,鼠标事件的获得的坐标是左上角为原点。因此需要在这里转换       this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1       this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1     };     window.addEventListener("mousemove", onMouse, false);   }   showTip () {     if (!this.dom) {       this.dom = document.createElement("p");       this.tip = new CSS2DObject(this.dom);     }     if (this.lastPick) {       const { x, y, z } = this.lastPick.point;       const properties = this.lastPick.object.parent.properties;       this.dom.className = "label";       this.dom.innerText = properties.name       this.tip.position.set(x + 10, y + 10, z);       this.map && this.map.add(this.tip);     }   }   render() {     this.raycaster.setFromCamera(this.mouse, this.camera)      const intersects = this.raycaster.intersectObjects(       this.scene.children,       true     )          // 如果this.lastPick存在,将材质颜色还原     if (this.lastPick) {       this.lastPick.object.material[0].color.set(MATERIAL_COLOR1);       this.lastPick.object.material[1].color.set(MATERIAL_COLOR2);     }     // 置空     this.lastPick = null;     // 查询当前鼠标移动所产生的射线与物体的焦点     // 有两个material的就是我们要找的对象     this.lastPick = intersects.find(       (item) => item.object.material && item.object.material.length === 2     );     // 找到后把颜色换成一个鲜艳的绿色     if (this.lastPick) {       this.lastPick.object.material[0].color.set("aquamarine");       this.lastPick.object.material[1].color.set("aquamarine");     }      this.showTip()      this.renderer.render(this.scene, this.camera)     this.css2dRenderer.render(this.scene, this.camera)     requestAnimationFrame(this.render.bind(this))   }   setControls() {     this.controls = new OrbitControls(this.camera, this.renderer.domElement)     // 太灵活了,来个阻尼     this.controls.enableDamping = true;     this.controls.dampingFactor = 0.1;   }   getGeoJson (adcode = "100000") {     return fetch(`https://geo.datav.aliyun.com/areas_v3/bound/${adcode}_full.json`)     .then(res => res.json())   }   async loadData(adcode) {     // 获取geojson数据     this.geojson = await this.getGeoJson(adcode)          // 创建墨卡托投影     this.projection = d3       .geoMercator()       .center([104.0, 37.5])       .translate([0, 0])          // Object3D是Three.js中大部分对象的基类,提供了一系列的属性和方法来对三维空间中的物体进行操纵。     // 初始化一个地图     this.map = new THREE.Object3D();     this.geojson.features.forEach(elem => {       const area = new THREE.Object3D()       // 坐标系数组(为什么是数组,因为有地区不止一个几何体,比如河北被北京分开了,比如舟山群岛)       const coordinates = elem.geometry.coordinates       const type = elem.geometry.type        // 定义一个画几何体的方法       const drawPolygon = (polygon) => {         // Shape(形状)。使用路径以及可选的孔洞来定义一个二维形状平面。 它可以和ExtrudeGeometry、ShapeGeometry一起使用,获取点,或者获取三角面。         const shape = new THREE.Shape()         // 存放的点位,最后需要用THREE.Line将点位构成一条线,也就是地图上区域间的边界线         // 为什么两个数组,因为需要三维地图的两面都画线,且它们的z坐标不同         let points1 = [];         let points2 = [];          for (let i = 0; i < polygon.length; i++) {           // 将经纬度通过墨卡托投影转换成threejs中的坐标           const [x, y] = this.projection(polygon[i]);           // 画二维形状           if (i === 0) {             shape.moveTo(x, -y);           }           shape.lineTo(x, -y);            points1.push(new THREE.Vector3(x, -y, 10));           points2.push(new THREE.Vector3(x, -y, 0));         }          /**          * ExtrudeGeometry (挤压缓冲几何体)          * 文档链接:https://threejs.org/docs/index.html?q=ExtrudeGeometry#api/zh/geometries/ExtrudeGeometry          */         const geometry = new THREE.ExtrudeGeometry(shape, {           depth: 10,           bevelEnabled: false,         });         /**          * 基础材质          */         // 正反两面的材质         const material1 = new THREE.MeshBasicMaterial({           color: MATERIAL_COLOR1,         });         // 侧边材质         const material2 = new THREE.MeshBasicMaterial({           color: MATERIAL_COLOR2,         });         // 生成一个几何物体(如果是中国地图,那么每一个mesh就是一个省份几何体)         const mesh = new THREE.Mesh(geometry, [material1, material2]);         area.add(mesh);          /**          * 画线          * link: https://threejs.org/docs/index.html?q=Line#api/zh/objects/Line          */         const lineGeometry1 = new THREE.BufferGeometry().setFromPoints(points1);         const lineGeometry2 = new THREE.BufferGeometry().setFromPoints(points2);         const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });         const line1 = new THREE.Line(lineGeometry1, lineMaterial);         const line2 = new THREE.Line(lineGeometry2, lineMaterial);         area.add(line1);         area.add(line2);          // 把地区属性存到area对象中         area.properties = elem.properties       }        // type可能是MultiPolygon 也可能是Polygon       if (type === "MultiPolygon") {         coordinates.forEach((multiPolygon) => {           multiPolygon.forEach((polygon) => {             drawPolygon(polygon);           });         });       } else {         coordinates.forEach((polygon) => {           drawPolygon(polygon);         });       }        // 把区域添加到地图中       this.map.add(area);      })      // 把地图添加到场景中     this.scene.add(this.map)   } }  const map = new Map3D()五、地图下钻
  现在除了地图下钻,都已经完成了。地图下钻其实就是把当前地图清空,然后再次调用一下 loadData 方法,传入adcode就可以创建对应地区的3D地图了。
  思路非常简单,先绑定点击事件,这里就不需要光线投射了,因为已经监听mousever事件了,并且数据已经存在this.lastPick这个变量中了。只需要在监听点击时获取选中的lastPick对象就可以了。
  然后调用this.loadData(areaId),不过...在调用loadData方法前需要将创建的地图清空,并且释放几何体和材质对象,防止内存泄露。
  理清思路后开始动手。
  首先绑定点击事件。我们在调用点击事件时,例如高德地图、echarts,会以 obj.on("click", callback)的形式调用,这样就不会局限于click事件了,双击事件以及其它的事件都可以监听和移除,那我们也试着这么做一个。在Map3D类中创建一个on 监听事件的方法和一个off 移除事件的方法。class Map3D{      constructor() {   	// 监听回调事件存储区     this.callbackStack = new Map();   }      // 省略代码......      // 添加监听事件   on(eventName, callback) {     const fnName = `${eventName}_fn`;     if (!this.callbackStack.get(eventName)) {       this.callbackStack.set(eventName, new Set());     }     if (!this.callbackStack.get(eventName).has(callback)) {       this.callbackStack.get(eventName).add(callback);     }     if (!this.callbackStack.get(fnName)) {       this.callbackStack.set(fnName, (e) => {         this.callbackStack.get(eventName).forEach((cb) => {           if (this.lastPick) cb(e, this.lastPick);         });       });     }     window.addEventListener(eventName, this.callbackStack.get(fnName));   }      // 移除监听事件   off(eventName, callback) {     const fnName = `${eventName}_fn`;     if (!this.callbackStack.get(eventName)) return;      if (this.callbackStack.get(eventName).has(callback)) {       this.callbackStack.get(eventName).delete(callback);     }     if (this.callbackStack.get(eventName).size < 1) {       window.removeEventListener(eventName, this.callbackStack.get(fnName));     }   }    }  const map = new Map3D();  map.on("click", listener)  function listener(e, data) {   // Mesh对象   console.log(data)   // 区域编码   console.log(data.object.parent.properties.adcode) }
  在上面的 listener 回调方法中打印可以获取到当前点击区域。
  先忍住调用loadData()方法,在此之前,要先抹掉之前一番操作搞出来的地图。
  在Map3D类中再创建一个dispose方法,用来移除地图以及释放内存class Map3D { // 省略代码......      dispose (o) {       // 可以遍历该父场景中的所有子物体来执行回调函数       o.traverse(child => {         if (child.geometry) {           child.geometry.dispose()         }         if (child.material) {           if (Array.isArray(child.material)) {             child.material.forEach(material => {               material.dispose()             })           } else {             child.material.dispose()           }         }       })       o.parent.remove(o)   }        // 省略代码......    } const map = new Map3D() map.on("click", listener) function listener(e, data) {   // 区域编码   const adcode = data.object.parent.properties.adcode    if(adcode) {     map.dispose(map.map)     map.loadData(adcode)   } }
  下钻
  现在已经可以下钻了,但是又出现了一个新问题[吐血]。到省份一级后,地图太小了,而且位置也没有在中间。这是由于我们的墨卡托投影 变换的中心点和缩放比例是写死的,我们需要让这些参数根据地理数据的不同而生成相对应的值。
  在geojson中,coordinates数组中的坐标就是这块区域的边界线上的点,以浙江省为例,只要找出浙江省边界线上点位的最大横向坐标(maxX)和最小横向坐标(minX),它们的和 / 2 就能得到X轴上的中心点。同理Y轴中心点也是如此。
  缩放倍数只需要根据画布的宽与浙江省横向长度比值和画布的高与浙江省纵向长度比值中取一个最小值再乘以一个系数(待定)。
  开始动手,在Map3D类中添加getCenter方法:class Map3D{   // 省略代码.....    	// 获取中心点和缩放倍数   getCenter() {     let maxX = undefined;     let maxY = undefined;     let minX = undefined;     let minY = undefined;     this.geoJson.features.forEach((elem) => {       const coordinates = elem.geometry.coordinates;       const type = elem.geometry.type;        function compare(point) {         maxX === undefined           ? (maxX = point[0])           : (maxX = point[0] > maxX ? point[0] : maxX);         maxY === undefined           ? (maxY = point[1])           : (maxY = point[1] > maxY ? point[1] : maxY);         minX === undefined           ? (minX = point[0])           : (minX = point[0] > minX ? minX : point[0]);         minY === undefined           ? (minY = point[1])           : (minY = point[1] > minY ? minY : point[1]);       }        if (type === "MultiPolygon") {         coordinates.forEach((multiPolygon) => {           multiPolygon.forEach((polygon) => {             polygon.forEach((point) => {               compare(point);             });           });         });       } else {         coordinates.forEach((polygon) => {           polygon.forEach((point) => {             compare(point);           });         });       }     });     const xScale = window.innerWidth / (maxX - minX);     const yScale = window.innerHeight / (maxY - minY);     return {       center: [(maxX + minX) / 2, (maxY + minY) / 2],       scale: Math.min(xScale, yScale),     };   }      async loadData(adcode) {     // 获取geojson数据     this.geojson = await this.getGeoJson(adcode)      const { center, scale } = this.getCenter()          // 创建墨卡托投影     this.projection = d3       .geoMercator()       .center(center)       .translate([0, 0])       .scale(scale * 7) // 根据实测,系数7差不多刚好   }      // 省略代码..... }
  看效果:
  下钻地图2
  完整代码:import * as THREE from "three" import * as d3 from "d3-geo" import { OrbitControls  } from "three/examples/jsm/controls/OrbitControls" import { CSS2DObject, CSS2DRenderer } from "three/examples/jsm/renderers/CSS2DRenderer"  const MATERIAL_COLOR1 = "#2887ee"; const MATERIAL_COLOR2 = "#2887d9";  class Map3D {   constructor() {     // 监听回调事件存储区     this.callbackStack = new Map();      this.scene = undefined  // 场景     this.camera = undefined // 相机     this.renderer = undefined // 渲染器     this.css2dRenderer = undefined // html渲染器     this.geojson = undefined // 地图json数据      this.init()   }   init() {     // 创建场景     this.scene = new THREE.Scene()      // 创建相机     this.setCamera()      // 创建渲染器     this.setRender()          // 创建控制器     this.setControls()      // 光线投射     this.setRaycaster()          // 加载数据     this.loadData()      // 渲染函数     this.render()    }   /**    * 创建相机    */   setCamera() {     // PerspectiveCamera(角度,长宽比,近端面,远端面) —— 透视相机     this.camera = new THREE.PerspectiveCamera(       75,       window.innerWidth / window.innerHeight,       0.1,       1000     )     // 设置相机位置     this.camera.position.set(0, 0, 120)     // 把相机添加到场景中     this.scene.add(this.camera)   }   /**    * 创建渲染器    */   setRender() {     this.renderer = new THREE.WebGLRenderer()     // 渲染器尺寸     this.renderer.setSize(window.innerWidth, window.innerHeight)     //设置背景颜色     this.renderer.setClearColor(0x000000)     // 将渲染器追加到dom中     document.body.appendChild(this.renderer.domElement)      // CSS2DRenderer 创建的是html的p元素     // 这里将p设置成绝对定位,盖住canvas画布     this.css2dRenderer = new CSS2DRenderer();     this.css2dRenderer.setSize(window.innerWidth, window.innerHeight);     this.css2dRenderer.domElement.style.position = "absolute";     this.css2dRenderer.domElement.style.top = "0px";     this.css2dRenderer.domElement.style.pointerEvents = "none";     document.body.appendChild(this.css2dRenderer.domElement);   }   setRaycaster() {     this.raycaster = new THREE.Raycaster();     this.mouse = new THREE.Vector2();     const onMouse = (event) => {       // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)       // threejs的三维坐标系是中间为原点,鼠标事件的获得的坐标是左上角为原点。因此需要在这里转换       this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1       this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1     };     window.addEventListener("mousemove", onMouse, false);   }   showTip () {     if (!this.dom) {       this.dom = document.createElement("p");       this.tip = new CSS2DObject(this.dom);     }     if (this.lastPick) {       const { x, y, z } = this.lastPick.point;       const properties = this.lastPick.object.parent.properties;       // label的样式在直接用css写在样式表中       this.dom.className = "label";       this.dom.innerText = properties.name       this.tip.position.set(x + 10, y + 10, z);       this.map && this.map.add(this.tip);     }   }   render() {     this.raycaster.setFromCamera(this.mouse, this.camera)      const intersects = this.raycaster.intersectObjects(       this.scene.children,       true     )          // 如果this.lastPick存在,将材质颜色还原     if (this.lastPick) {       this.lastPick.object.material[0].color.set(MATERIAL_COLOR1);       this.lastPick.object.material[1].color.set(MATERIAL_COLOR2);     }     // 置空     this.lastPick = null;     // 查询当前鼠标移动所产生的射线与物体的焦点     // 有两个material的就是我们要找的对象     this.lastPick = intersects.find(       (item) => item.object.material && item.object.material.length === 2     );     // 找到后把颜色换成一个鲜艳的绿色     if (this.lastPick) {       this.lastPick.object.material[0].color.set("aquamarine");       this.lastPick.object.material[1].color.set("aquamarine");     }      this.showTip()      this.renderer.render(this.scene, this.camera)     this.css2dRenderer.render(this.scene, this.camera)     requestAnimationFrame(this.render.bind(this))   }   setControls() {     this.controls = new OrbitControls(this.camera, this.renderer.domElement)     // 太灵活了,来个阻尼     this.controls.enableDamping = true;     this.controls.dampingFactor = 0.1;   }   getGeoJson (adcode = "100000") {     return fetch(`https://geo.datav.aliyun.com/areas_v3/bound/${adcode}_full.json`)     .then(res => res.json())   }   // 获取中心点和缩放倍数   getCenter () {     let maxX, maxY, minX, minY;     this.geojson.features.forEach((elem) => {       const coordinates = elem.geometry.coordinates;       const type = elem.geometry.type;        function compare (point) {         maxX === undefined           ? (maxX = point[0])           : (maxX = point[0] > maxX ? point[0] : maxX);         maxY === undefined           ? (maxY = point[1])           : (maxY = point[1] > maxY ? point[1] : maxY);         minX === undefined           ? (minX = point[0])           : (minX = point[0] > minX ? minX : point[0]);         minY === undefined           ? (minY = point[1])           : (minY = point[1] > minY ? minY : point[1]);       }        if (type === "MultiPolygon") {         coordinates.forEach((multiPolygon) => {           multiPolygon.forEach((polygon) => {             polygon.forEach((point) => {               compare(point);             });           });         });       } else {         coordinates.forEach((polygon) => {           polygon.forEach((point) => {             compare(point);           });         });       }     });     const xScale = window.innerWidth / (maxX - minX);     const yScale = window.innerHeight / (maxY - minY);     return {       center: [(maxX + minX) / 2, (maxY + minY) / 2],       scale: Math.min(xScale, yScale),     };   }   async loadData(adcode) {     // 获取geojson数据     this.geojson = await this.getGeoJson(adcode)      const { center, scale } = this.getCenter()          // 创建墨卡托投影     this.projection = d3       .geoMercator()       .center(center)       .translate([0, 0])       .scale(scale * 7) // 根据实测,系数7差不多刚好          // Object3D是Three.js中大部分对象的基类,提供了一系列的属性和方法来对三维空间中的物体进行操纵。     // 初始化一个地图     this.map = new THREE.Object3D();     this.geojson.features.forEach(elem => {       const area = new THREE.Object3D()       // 坐标系数组(为什么是数组,因为有地区不止一个几何体,比如河北被北京分开了,比如舟山群岛)       const coordinates = elem.geometry.coordinates       const type = elem.geometry.type        // 定义一个画几何体的方法       const drawPolygon = (polygon) => {         // Shape(形状)。使用路径以及可选的孔洞来定义一个二维形状平面。 它可以和ExtrudeGeometry、ShapeGeometry一起使用,获取点,或者获取三角面。         const shape = new THREE.Shape()         // 存放的点位,最后需要用THREE.Line将点位构成一条线,也就是地图上区域间的边界线         // 为什么两个数组,因为需要三维地图的两面都画线,且它们的z坐标不同         let points1 = [];         let points2 = [];          for (let i = 0; i < polygon.length; i++) {           // 将经纬度通过墨卡托投影转换成threejs中的坐标           const [x, y] = this.projection(polygon[i]);           // 画二维形状           if (i === 0) {             shape.moveTo(x, -y);           }           shape.lineTo(x, -y);            points1.push(new THREE.Vector3(x, -y, 10));           points2.push(new THREE.Vector3(x, -y, 0));         }          /**          * ExtrudeGeometry (挤压缓冲几何体)          * 文档链接:https://threejs.org/docs/index.html?q=ExtrudeGeometry#api/zh/geometries/ExtrudeGeometry          */         const geometry = new THREE.ExtrudeGeometry(shape, {           depth: 10,           bevelEnabled: false,         });         /**          * 基础材质          */         // 正反两面的材质         const material1 = new THREE.MeshBasicMaterial({           color: MATERIAL_COLOR1,         });         // 侧边材质         const material2 = new THREE.MeshBasicMaterial({           color: MATERIAL_COLOR2,         });         // 生成一个几何物体(如果是中国地图,那么每一个mesh就是一个省份几何体)         const mesh = new THREE.Mesh(geometry, [material1, material2]);         area.add(mesh);          /**          * 画线          * link: https://threejs.org/docs/index.html?q=Line#api/zh/objects/Line          */         const lineGeometry1 = new THREE.BufferGeometry().setFromPoints(points1);         const lineGeometry2 = new THREE.BufferGeometry().setFromPoints(points2);         const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });         const line1 = new THREE.Line(lineGeometry1, lineMaterial);         const line2 = new THREE.Line(lineGeometry2, lineMaterial);         area.add(line1);         area.add(line2);          // 把地区属性存到area对象中         area.properties = elem.properties       }        // type可能是MultiPolygon 也可能是Polygon       if (type === "MultiPolygon") {         coordinates.forEach((multiPolygon) => {           multiPolygon.forEach((polygon) => {             drawPolygon(polygon);           });         });       } else {         coordinates.forEach((polygon) => {           drawPolygon(polygon);         });       }        // 把区域添加到地图中       this.map.add(area);      })      // 把地图添加到场景中     this.scene.add(this.map)   }   dispose (o) {     // 可以遍历该父场景中的所有子物体来执行回调函数     o.traverse(child => {       if (child.geometry) {         child.geometry.dispose()       }       if (child.material) {         if (Array.isArray(child.material)) {           child.material.forEach(material => {             material.dispose()           })         } else {           child.material.dispose()         }       }     })     o.parent.remove(o)   }    // 添加监听事件   on (eventName, callback) {     const fnName = `${eventName}_fn`;     if (!this.callbackStack.get(eventName)) {       this.callbackStack.set(eventName, new Set());     }     if (!this.callbackStack.get(eventName).has(callback)) {       this.callbackStack.get(eventName).add(callback);     }     if (!this.callbackStack.get(fnName)) {       this.callbackStack.set(fnName, (e) => {         this.callbackStack.get(eventName).forEach((cb) => {           if (this.lastPick) cb(e, this.lastPick);         });       });     }     window.addEventListener(eventName, this.callbackStack.get(fnName));   }    // 移除监听事件   off (eventName, callback) {     const fnName = `${eventName}_fn`;     if (!this.callbackStack.get(eventName)) return;      if (this.callbackStack.get(eventName).has(callback)) {       this.callbackStack.get(eventName).delete(callback);     }     if (this.callbackStack.get(eventName).size < 1) {       window.removeEventListener(eventName, this.callbackStack.get(fnName));     }   } }  const map = new Map3D()  map.on("click", listener)  function listener(e, data) {   // 区域编码   const adcode = data.object.parent.properties.adcode    if(adcode) {     map.dispose(map.map)     map.loadData(adcode)   } }

福建号有多先进?电磁弹射只是部分,美国未攻克的难题被中国拿下世界上只有两条采用电磁弹射器的航空母舰,一是我国刚刚下水的福建号,另一个是美国的最新航母福特号。这两个航空母舰的电磁弹射器,我国采用的是直流电,美国采用的是交流电,我国的电磁弹射器外国人心目中的习近平引领中国经济巨轮破浪前行前所未有人民生活水平普遍提升向高科技高质量增长转型对世界产生重大影响外国专家如此形容过去十年习近平经济思想领航定向中国经济取得的巨大成就作为习近平经济思想的灵魂新发展理念的提出为何老得慢的男人,平常都爱吃这5种食物,吃过3个,就该偷着乐俗话说男人三十一枝花,女人三十豆腐渣,这是从年龄上我们得出的结论,但是女性爱美丽更要注意保养身材,而很多男性朋友因为工作的原因,在三十岁就显得有些富态和油腻了。人从一出生就开始在慢我曾在奇葩说里看到过一句话情绪的尽头是沉默我曾在奇葩说里看到过这样一句话一个人的情绪到了尽头,就是沉默。情绪的释放,从有人过问,到习以为常,再到无人问津,到最后才知道。沉默才是尽头。大地之灯里有说过这样一句话世上有诸多为自降低痴呆风险的饮食方案MIND饮食,中文常译为心智饮食心智饮食其核心目的在于延缓个体认知下降降低痴呆风险。心智饮食的内容,结合了DASH饮食(得舒饮食),即一种控制高血压的饮食方法,以及地中海饮食。恰巧普京圣彼得堡讲话揭露西方真实嘴脸!美国或联合盟友加大制裁力度普京在圣彼得堡经济论坛上,发表了极为重要的讲话。在这次讲话中,普京对前一阶段,俄罗斯与西方的博弈,进行了全面总结。同时,也通过各种数据和事例,揭露了西方国家的丑恶嘴脸。被撕下伪装的国羽4胜3负!六人晋级决赛冲击冠军,赵俊鹏再创佳绩,日本队会师2022年6月18日,羽毛球印尼大师赛继续进行,决赛对阵出炉,国羽4胜3负,赵俊鹏王祉怡郑思维黄雅琼刘雨辰欧烜屹晋级,陈雨菲何冰娇王懿律黄东萍出局,三位奥运冠军被淘汰,最大的惊喜来美议员窜访台湾后炮制挺台法案据台媒援引路透社报道,今年4月曾窜访台湾地区,并与蔡英文等人碰面的美国两位参议员梅南德兹和格雷厄姆,日前推出一项有意大幅提升对台支持的立法,包括美国对台提供数十亿美元的安全援助,并徐汝清我的文章插上了翅膀作者清风徐来(江苏宝应徐汝清)一天,老友潘久海转送给我一封海外来信,信封上写着中国江苏省宝应县司法局,请潘久海梁鼎臣速转徐汝清同志收!这封信寄自美国!我当即展阅我是粱玉林,是您在宝他曾创造奇迹,期待他再次上演王者归来北京时间6月18日下午,布达佩斯游泳世锦赛正式拉开帷幕,中国游泳队多位名将在首个比赛日亮相。东京奥运会冠军张雨霏汤慕涵均顺利通过预赛,而另一位冲击大满贯的是老将汪顺未能晋级男子40爱情电影推荐海吉拉(主演李光汉)超感人好几年前看过这部电影,到现在为止还是觉得挺遗憾的。豆瓣评分为7。1分,分数不高,但是值得一看。两个性格迥异的女性却意外成了最好的姐妹何希真刘宛婷,他们同时喜欢上文棠生,但其实文棠生
恩比德三节砍38分13板5助4封盖,76人狂胜黄蜂获7连胜76人客场12182轻取黄蜂获得7连胜。开场哈登连线恩比德空接,哈里斯命中三分,哈登突破上篮。黄蜂方面罗齐尔连中三分,海沃德中投造罚球,黄蜂149领先。哈登突破打进,恩比德连拿5分4比3!孙颖莎险胜朱芊曦,21岁的国乒弃将,把莎莎差点逼入绝境国乒要警惕了,一个比伊藤美诚还难缠的对手出现了。3月17日,WTT新加坡赛继续进行,国乒一姐孙颖莎以4比3击败了韩国小将朱芊曦。比赛打得很激烈,孙颖莎在前四局打了一个3比1,就当球曼城总监我们一直都很尊重拜仁,但也已经准备好战斗直播吧3月17日讯欧冠四分之一决赛对阵已经出炉,曼城将和拜仁争夺一个半决赛的名额。曼城足球总监贝吉里斯坦表示,球队已经准备好战斗。他们当然非常强大,贝吉里斯坦说。拜仁和我们一样是争CBA最新排名出炉!前4格局已定,57名难分胜负,6队无缘季后赛CBA常规赛第36轮开打,这也是意味着常规赛进入到倒计时了,42轮常规赛决出前12名进入到季后赛争夺。随着深圳男篮崛起,一波9连胜锁定了最后一个四强的名额,毕竟深圳(25胜11负)南京鸡鸣寺穿和服拍照的女子到底美不美刚看了一个新闻,心里突然就生气了团火,朋友们你们说她美吗?事情的经过是这样的3月20日一位网友在南京鸡鸣寺游览时看到一位身穿白色和服的女子在拍照,作为一个爱国人士,这位网友忍住愤怒纪南文旅区将建国家A级卡丁车赛车场集合速度与激情的卡丁车项目,深受年轻人的喜爱。随着不断发展,卡丁车运动已从单纯的休闲娱乐项目,逐渐发展成为一项成熟的赛车运动。记者从纪南文旅区获悉,近日,该区与深圳市前海独秀科技有郑新立发展县域经济是扩大内需和乡村振兴的重要举措中国小康网讯记者袁帅3月22日,由天道创服集团发起,亚洲财富论坛光彩四十九控股小康杂志社联合主办的2023中国县域经济发展大会(CCEDC)(简称县发会)在海南海口开幕。来自政府学2022年湖南年平均气温近百年来第二,年高温日数再创新高!视频加载中三湘都市报3月23日讯(文视频全媒体记者李致远)今天是第63个世界气象日,主题为天气气候水,代代向未来。3月23日,湖南省气象局召开世界气象日新闻发布会,2022年度湖南老奶洋芋奶茶是怎么研发出来的前段时间,云南一奶茶品牌推出的老奶洋芋奶茶成为网友热议的对象。这让不少网友对奶茶研发师这一职业产生了兴趣到底是怎样的脑洞,才能研发出这样一杯奶茶?日前,记者找到研发老奶洋芋奶茶的9政府问计于企的早餐会多多益善证券时报记者孙勇这两天,由浙江省委常委杭州市委书记刘捷做东,宴请企业家的一顿早餐,成为热点新闻。受邀与刘捷共进早餐的企业家,来自阿里巴巴浙江恒逸蚂蚁科技海康威视吉利控股网易网络等1大结局!张隆李梦甜蜜成婚,前妻撤诉,姚明重拳出击,离女篮赴美轰轰烈烈的李梦知三当三事件迎来大结局,张隆和李梦确认已经成婚,前妻赵蕾也是选择放手。毕竟,看得出来李梦深爱着张隆,既然他们已经领证,就应该成人之美,大度送上祝福。而在这次的风波过后