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

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)   } }

痛经是小问题吗?这种痛经会影响生育?如何缓解痛经呢?痛经是多数女人的烦恼,轻者热敷可以缓解,重者需服止痛药才能缓解,也有用药依然疼痛需请假卧床休息的。引起痛经的原因有很多种的,年轻的女性都是原发性的痛经,需要慢慢地调理,但是有一种情存款利率调降后,万能险收益也降了!结算利率集中在4至4。7,已有产品跌破4每经记者袁园每经编辑廖丹继多家银行下调存款利率之后,部分保险公司万能险产品结算利率也跟着出现了下滑。有业内人士表示,万能险结算利率下滑的重要原因是股市低迷市场利率下行导致金融投资收农村有一种河鲜,20多年前随处可见,如今想吃完全靠运气在我们农村的河溪里一种很苦的鱼,过去没人吃,如今备受青睐。在日常生活里,淡水鱼是我们最常见的肉类食品,鱼肉脂肪少肉质细腻,相比较于其它肉食来说,鱼肉含有的蛋白质,维生素以及钙锌等营理财公司探索专精特新新蓝海产品破净尴尬如何缓解理财公司正逐步拓展理财产品资源,布局新兴领域主题理财产品。近日工银理财推出首只专精特新主题理财产品,并已开启募集。9月20日,北京商报记者从工银理财处获悉,该只理财产品以固定收益类豫健中医药膳坊解个大便出一身汗,该怎么办?粪质不干却解不出来,这种尴尬可不是仅仅出现在气机郁滞型便秘的人群中,还有一类人更加凄惨,每每因为解个大便而汗流浃背,上个厕所就如同从事一场重体力劳动,整个人都不好了。这就是气虚阳衰(图文)沙特拉比格2660MW亚临界燃油电站项目请关注本头条号带您了解我国电力的海外事业!并领略一个免费旅游的海外漂泊圈!沙特ACWA国际电力公司,韩国电力公司,沙特电力公司联合投资,总承包商是中国电建旗下的山东电建三公司,石油外包公司有资格食用本公司的下午茶等福利吗?外包员工有资格食用本公司的下午茶等福利吗?对于外包公司,很多人或许会有偏见,经常有本公司的员工看不起外包公司员工的新闻发生。认为外包员工要么就是能力差,要么学历低门槛低,但是很多大监管摸底券商研究业务经营情况重点关注分析师构成机构客户数量及佣金收入等监管摸底券商研究业务经营情况重点关注分析师构成机构客户数量及佣金收入等财联社9月21日电,从券商人士处获悉,中国证券业协会日前对2021年证券公司发布证券研究报告业务经营情况进行了潘通发布2023春夏流行色!10种首选5种经典你喜欢吗?还有不到2周,就要迎来大家梦寐以求的国庆小长假了,是不是很期待?不过也不要高兴得太早,毕竟这是今年的最后一个假期了!话说2022年只剩最后3个月了,作为全球色彩权威机构的Panto南财快评如何看待监管强调严控城商行理财业务规模最近多家城商行收到属地监管部门强调严控理财业务规模的提示函。从监管趋势上来看,对城商行的理财业务规模进行管控,乃至压缩,会成为一段时间以内金融监管的主题。其表面的原因,是当前各类城青岛银行状告股东旗下公司近年发力理财业务后投资纠纷增多因3。5亿美元委托投资协议出现纠纷,青岛银行将股东旗下公司告上法庭中国科技投资张婷龙敏近日,青岛银行(002948。SZ)发布公告称,就与AMTDGLOBALMARKETSLIMI
11月轿车销量丨轩逸跌至第五,合资普跌,自主借力新能源走俏受近期持续疫情影响,11月份汽车销量如我们所预料的那样出现同比下降。从乘联会最新零售销量数据来看,11月份国内狭义乘用车市场零售销量达164。8万辆,同比下降9。3,环比下降10。有的人阳了,想着让别人也阳,有的人没阳,想着阳个好阳有的人阳了,想着让别人也阳,有的人没阳,想着阳个好阳。一个阳让我们看清了世间百态。短短一周的时间,阳遍了整个城市,大家的神经都在紧张着。取消了全员核酸检测,取消了阳强制隔离,当自觉祈祷苍生不合群!一个人的失败,并不取决于他是否合群,更不取决于别人对他的批评和强加在他身上的标签。我为天下苍生祈祷都好好的,都要不合群不入群远离羊群!这一宿几乎没有深度睡眠,做梦还是连续剧,剧名叫慢慢的,它们就没有了,就像从未存在人一旦年纪大了,就喜欢去回忆那些走过的日子。因为那些逝去的日子有我们的青春和纯真。在这个物欲横流的时代,那些单纯而美好的事物终于被淘汰被扔进时间的垃圾桶。还记得么,你是怎么套路自己一座城一段历史25日清晨我们驾车从泸水向记忆之城进发。沿着美丽公路顺怒江逆流而上,经过金银滩虎跳石顺着蜿蜒盘旋而上的公路到达老姆登村。老姆登村是怒江峡谷中一个以怒族人为主,傈僳族独龙族杂居的村寨小小文案2。温柔和爱才是生活的止痛剂。Tendernessandlovearethepainkillersoflife。3。万事都要全力以赴包括开心Doeverythingyoucanin任正非的血色浪漫王小波的半路浪漫刘强东的超前浪漫你为什么不离婚?任正非的血色浪漫对作家的降维打击,王小波的浪漫转型创业的慈悲,刘强东的超前浪漫华为的一位中层领导来辞职,理由是工作影响陪伴妻子,任正非的回应与众不同,他不能理解地问为这样的东哥点赞如果情况属实,我为东哥点赞!据说,刘强东卸任京东CEO后,当上了平石头村村长,上任后第一件事就是建一所最好的小学,承诺老师工资由他来发,每月工资不低于一万!曾经看报道,说东哥回家过旅游业要回来了旅游业要回来了!文化和旅游部9日发布新版疫情防控工作指南除导游上团前核验健康码外,对旅游景区旅行社等不再要求提供核酸检测阴性证明查验健康码开展落地检。随着疫情防控措施持续优化,国内楠溪江旅游工程建设硕果累累,邀您一起分享近年来,温州市楠溪江旅游经济发展中心工程建设处作为楠溪江旅游项目建设设计与施工的参与者实施者和推动者,深入贯彻县委县政府决策部署,大力实施12310旅游发展路径,打造楠溪江乡村音乐双十二高性价比手机OPPOK10天玑800067瓦闪充8256仅售1519手机型号OPPOK10发售日期2022。4。28特点熊猫一强玻璃,天玑8000max性能铁三角,蓝牙5。3,3。5mm耳机孔,NFC。外观6。59英寸天马微电子1080P分辨率LC