Threejs绘制地图(geojson)

https://juejin.im/post/5e344733e51d453ce13d2579

目前接触了一些室内地图的开发工作,二维的、三维的,数据源基本都是采用geojson格式

基于geojson的地图绘制目前已经有比较成熟的框架和解决方案了。

但是今天我们还是要在Threejs里来简单实现一下三维数据的展示。

代码地址 预览地址

主要实现了2个功能

  • 三维地图展示
  • POI信息显示

数据采集

首先我们需要一份中国省份的轮廓数据

在这份数据我们需要的字段有

  • properties.name

    用于POI信息的展示

  • properties.centroid

    用于POI信息的定位

  • geometry.coordinates

    用于构建我们的三维模型

三维环境搭建

注意

  • 以下仅是核心代码

  • 绿色的线是y轴

  • 红色的线是x轴

  • 蓝色的线是z轴

    1mounted () { 2 // 初始化3D环境 3 this.initEnvironment() 4 // 构建光照系统 5 this.buildLightSystem() 6 // 构建辅助系统 7 this.buildAuxSystem() 8}, 9methods: { 10 // 初始化3D环境 11 initEnvironment () { 12 this.scene = new THREE.Scene(); 13 this.scene.background = new THREE.Color(0xf0f0f0) 14 // 建一个空对象存放对象 15 this.map = new THREE.Object3D() 16 // 设置相机参数 17 this.setCamera(); 18 // 初始化 19 this.renderer = new THREE.WebGLRenderer({ 20 alpha: true, 21 canvas: document.querySelector('canvas') 22 }) 23 this.renderer.setPixelRatio(window.devicePixelRatio) 24 this.renderer.setSize(window.innerWidth, window.innerHeight - 10) 25 document.addEventListener('mousemove', this.onDocumentMouseMove, false) 26 window.addEventListener('resize', this.onWindowResize, false) 27 }, 28 setCamera () { 29 this.camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 10000); 30 this.camera.position.set(0, -70, 90); 31 this.camera.lookAt(0, 0, 0); 32 }, 33 // 构建辅助系统: 网格和坐标 34 buildAuxSystem () { 35 let axisHelper = new THREE.AxesHelper(2000) 36 this.scene.add(axisHelper) 37 let gridHelper = new THREE.GridHelper(600, 60) 38 this.scene.add(gridHelper) 39 let controls = new THREE.OrbitControls(this.camera, this.renderer.domElement) 40 controls.enableDamping = true 41 controls.dampingFactor = 0.25 42 controls.rotateSpeed = 0.35 43 }, 44 // 光照系统 45 buildLightSystem () { 46 let directionalLight = new THREE.DirectionalLight(0xffffff, 1.1); 47 directionalLight.position.set(300, 1000, 500); 48 directionalLight.target.position.set(0, 0, 0); 49 directionalLight.castShadow = true; 50 51 let d = 300; 52 const fov = 45 //拍摄距离 视野角值越大,场景中的物体越小 53 const near = 1 //相机离视体积最近的距离 54 const far = 1000//相机离视体积最远的距离 55 const aspect = window.innerWidth / window.innerHeight; //纵横比 56 directionalLight.shadow.camera = new THREE.PerspectiveCamera(fov, aspect, near, far); 57 directionalLight.shadow.bias = 0.0001; 58 directionalLight.shadow.mapSize.width = directionalLight.shadow.mapSize.height = 1024; 59 this.scene.add(directionalLight) 60 61 let light = new THREE.AmbientLight(0xffffff, 0.6) 62 this.scene.add(light) 63 64 }, 65 // 根据浏览器窗口变化动态更新尺寸 66 onWindowResize () { 67 this.camera.aspect = window.innerWidth / window.innerHeight; 68 this.camera.updateProjectionMatrix(); 69 this.renderer.setSize(window.innerWidth, window.innerHeight); 70 }, 71 onDocumentMouseMove (event) { 72 event.preventDefault(); 73 } 74}

    复制代码

绘制地图模型

数据分析

接下来我们需要根据 geometry.coordinates 来绘制地图

1 "geometry": { 2 "type": "MultiPolygon", 3 "coordinates": [ 4 [ 5 [ 6 [ 7 117.210024, 8 40.082262 9 ], 10 [ 11 117.105315, 12 40.074479 13 ], 14 [ 15 117.105315, 16 40.074479 17 ], 18 ... 19 ] 20 ] 21 ] 22 } 23 24复制代码

坐标转化

我们的坐标数据是经纬度坐标,我们需要把它转化成平面坐标 这里用到了 d3-geo 的坐标转化方法

多面绘制

注意这里的类型是 MultiPolygon(多面),我们的坐标点是嵌套在多层数组里面的。

因为我们的数据中,

有的省份轮廓是闭合的

有的省份是多个部分组成的

代码实现

我们的模型分成2部分

  1. 主体部分:我们用THREE.Shape() + THREE.ExtrudeGeometry()来实现

  2. 轮廓线部分:我们用THREE.Line()来实现

    1 initMap () { 2 // d3-geo转化坐标 3 const projection = d3geo.geoMercator().center([104.0, 37.5]).scale(80).translate([0, 0]); 4 // 遍历省份构建模型 5 chinaJson.features.forEach(elem => { 6 // 新建一个省份容器:用来存放省份对应的模型和轮廓线 7 const province = new THREE.Object3D() 8 const coordinates = elem.geometry.coordinates 9 coordinates.forEach(multiPolygon => { 10 multiPolygon.forEach(polygon => { 11 // 这里的坐标要做2次使用:1次用来构建模型,1次用来构建轮廓线 12 const shape = new THREE.Shape() 13 const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff }) 14 const linGeometry = new THREE.Geometry() 15 for (let i = 0; i < polygon.length; i++) { 16 const [x, y] = projection(polygon[i]) 17 if (i === 0) { 18 shape.moveTo(x, -y) 19 } 20 shape.lineTo(x, -y); 21 linGeometry.vertices.push(new THREE.Vector3(x, -y, 4.01)) 22 } 23 const extrudeSettings = { 24 depth: 4, 25 bevelEnabled: false 26 }; 27 const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings) 28 const material = new THREE.MeshBasicMaterial({ color: '#d13a34', transparent: true, opacity: 0.6 }) 29 const mesh = new THREE.Mesh(geometry, material) 30 const line = new THREE.Line(linGeometry, lineMaterial) 31 province.add(mesh) 32 province.add(line) 33 }) 34 }) 35 // 将geojson的properties放到模型中,后面会用到 36 province.properties = elem.properties 37 if (elem.properties.centroid) { 38 const [x, y] = projection(elem.properties.centroid) 39 province.properties._centroid = [x, y] 40 } 41 this.map.add(province) 42 }) 43 this.scene.add(this.map) 44 }

    复制代码

实现后的效果是这样

POI信息显示

如果是在室内地图的开发,我们通常会需要显示模块的一些信息,比如名称、图标之类的。这里我们就简单显示一下省份的名称就好。

我的做法是:

  1. 获取每个省份模块的中心点坐标,并转化成屏幕坐标

  2. 新建一个canvas,将省份名称根据坐标绘制到canvas上

  3. 解决坐标的碰撞问题

代码实现

1 showName () { 2 const width = window.innerWidth 3 const height = window.innerHeight 4 let canvas = document.querySelector('#name') 5 if (!canvas) return 6 canvas.width = width; 7 canvas.height = height; 8 const ctx = canvas.getContext('2d'); 9 // 新建一个离屏canvas 10 const offCanvas = document.createElement('canvas') 11 offCanvas.width = width 12 offCanvas.height = height 13 const ctxOffCanvas = canvas.getContext('2d'); 14 // 设置canvas字体样式 15 ctxOffCanvas.font = '16.5px Arial'; 16 ctxOffCanvas.strokeStyle = '#FFFFFF'; 17 ctxOffCanvas.fillStyle = '#000000'; 18 // texts用来存储显示的名称,重叠的部分就不会放到里面 19 const texts = []; 20 /** 21 * 遍历省份数据,有2个核心功能 22 * 1. 将3维坐标转化成2维坐标 23 * 2. 后面遍历到的数据,要和前面的数据做碰撞对比,重叠的就不绘制 24 * */ 25 this.map.children.forEach((elem, index) => { 26 if (!elem.properties._centroid) return 27 // 找到中心点 28 const y = -elem.properties._centroid[1] 29 const x = elem.properties._centroid[0] 30 const z = 4 31 // 转化为二维坐标 32 const vector = new THREE.Vector3(x, y, z) 33 const position = vector.project(this.camera) 34 // 构建文本的基本属性:名称,left, top, width, height -> 碰撞对比需要这些坐标数据 35 const name = elem.properties.name 36 const left = (vector.x + 1) / 2 * width 37 const top = -(vector.y - 1) / 2 * height 38 const text = { 39 name, 40 left, 41 top, 42 width: ctxOffCanvas.measureText(name).width, 43 height: 16.5 44 } 45 // 碰撞对比 46 let show = true 47 for (let i = 0; i < texts.length; i++) { 48 if ( 49 (text.left + text.width) < texts[i].left || 50 (text.top + text.height) < texts[i].top || 51 (texts[i].left + texts[i].width) < text.left || 52 (texts[i].top + texts[i].height) < text.top 53 ) { 54 show = true 55 } else { 56 show = false 57 break 58 } 59 } 60 if (show) { 61 texts.push(text) 62 ctxOffCanvas.strokeText(name, left, top) 63 ctxOffCanvas.fillText(name, left, top) 64 } 65 }) 66 // 离屏canvas绘制到canvas中 67 ctx.drawImage(offCanvas, 0, 0) 68 } 69复制代码

注意,因为我们的canvas是叠在threejs的canvas上仅作为展示的,所以需要加个样式 pointer-events: none;

谢谢阅读

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )