Threejs绘制地图(geojson)

Easter79
• 阅读 2289

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

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

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

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

代码地址 预览地址

主要实现了2个功能

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

数据采集

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

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

  • properties.name

    用于POI信息的展示

  • properties.centroid

    用于POI信息的定位

  • geometry.coordinates

    用于构建我们的三维模型

三维环境搭建

注意

  • 以下仅是核心代码

  • 绿色的线是y轴

  • 红色的线是x轴

  • 蓝色的线是z轴

    mounted () {
        // 初始化3D环境
        this.initEnvironment()
        // 构建光照系统
        this.buildLightSystem()
        // 构建辅助系统
        this.buildAuxSystem()
    },
    methods: {
        // 初始化3D环境
        initEnvironment () {
            this.scene = new THREE.Scene();
            this.scene.background = new THREE.Color(0xf0f0f0)
            // 建一个空对象存放对象
            this.map = new THREE.Object3D()
            // 设置相机参数
            this.setCamera();
            // 初始化
            this.renderer = new THREE.WebGLRenderer({
                alpha: true,
                canvas: document.querySelector('canvas')
            })
            this.renderer.setPixelRatio(window.devicePixelRatio)
            this.renderer.setSize(window.innerWidth, window.innerHeight - 10)
            document.addEventListener('mousemove', this.onDocumentMouseMove, false)
            window.addEventListener('resize', this.onWindowResize, false)
        },
        setCamera () {
            this.camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 10000);
            this.camera.position.set(0, -70, 90);
            this.camera.lookAt(0, 0, 0);
        },
        // 构建辅助系统: 网格和坐标
        buildAuxSystem () {
            let axisHelper = new THREE.AxesHelper(2000)
            this.scene.add(axisHelper)
            let gridHelper = new THREE.GridHelper(600, 60)
            this.scene.add(gridHelper)
            let controls = new THREE.OrbitControls(this.camera, this.renderer.domElement)
            controls.enableDamping = true
            controls.dampingFactor = 0.25
            controls.rotateSpeed = 0.35
        },
        // 光照系统
        buildLightSystem () {
            let directionalLight = new THREE.DirectionalLight(0xffffff, 1.1);
            directionalLight.position.set(300, 1000, 500);
            directionalLight.target.position.set(0, 0, 0);
            directionalLight.castShadow = true;
    
            let d = 300;
            const fov = 45 //拍摄距离  视野角值越大,场景中的物体越小
            const near = 1 //相机离视体积最近的距离
            const far = 1000//相机离视体积最远的距离
            const aspect = window.innerWidth / window.innerHeight; //纵横比
            directionalLight.shadow.camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
            directionalLight.shadow.bias = 0.0001;
            directionalLight.shadow.mapSize.width = directionalLight.shadow.mapSize.height = 1024;
            this.scene.add(directionalLight)
    
            let light = new THREE.AmbientLight(0xffffff, 0.6)
            this.scene.add(light)
    
        },
        // 根据浏览器窗口变化动态更新尺寸
        onWindowResize () {
            this.camera.aspect = window.innerWidth / window.innerHeight;
            this.camera.updateProjectionMatrix();
            this.renderer.setSize(window.innerWidth, window.innerHeight);
        },
        onDocumentMouseMove (event) {
            event.preventDefault();
        }
    }
    

    复制代码

绘制地图模型

数据分析

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

            "geometry": {
                "type": "MultiPolygon",
                "coordinates": [
                    [
                        [
                            [
                                117.210024,
                                40.082262
                            ],
                            [
                                117.105315,
                                40.074479
                            ],
                            [
                                117.105315,
                                40.074479
                            ],
                            ...
                        ]
                    ]
                ]
            }

复制代码

坐标转化

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

多面绘制

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

因为我们的数据中,

有的省份轮廓是闭合的

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

代码实现

我们的模型分成2部分

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

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

        initMap () {
            // d3-geo转化坐标
            const projection = d3geo.geoMercator().center([104.0, 37.5]).scale(80).translate([0, 0]);
            // 遍历省份构建模型
            chinaJson.features.forEach(elem => {
                // 新建一个省份容器:用来存放省份对应的模型和轮廓线
                const province = new THREE.Object3D()
                const coordinates = elem.geometry.coordinates
                coordinates.forEach(multiPolygon => {
                    multiPolygon.forEach(polygon => {
                        // 这里的坐标要做2次使用:1次用来构建模型,1次用来构建轮廓线
                        const shape = new THREE.Shape()
                        const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff })
                        const linGeometry = new THREE.Geometry()
                        for (let i = 0; i < polygon.length; i++) {
                            const [x, y] = projection(polygon[i])
                            if (i === 0) {
                                shape.moveTo(x, -y)
                            }
                            shape.lineTo(x, -y);
                            linGeometry.vertices.push(new THREE.Vector3(x, -y, 4.01))
                        }
                        const extrudeSettings = {
                            depth: 4,
                            bevelEnabled: false
                        };
                        const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings)
                        const material = new THREE.MeshBasicMaterial({ color: '#d13a34', transparent: true, opacity: 0.6 })
                        const mesh = new THREE.Mesh(geometry, material)
                        const line = new THREE.Line(linGeometry, lineMaterial)
                        province.add(mesh)
                        province.add(line)
                    })
                })
                // 将geojson的properties放到模型中,后面会用到
                province.properties = elem.properties
                if (elem.properties.centroid) {
                    const [x, y] = projection(elem.properties.centroid)
                    province.properties._centroid = [x, y]
                }
                this.map.add(province)
            })
            this.scene.add(this.map)
        }
    

    复制代码

实现后的效果是这样

POI信息显示

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

我的做法是:

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

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

  3. 解决坐标的碰撞问题

代码实现

        showName () {
            const width = window.innerWidth
            const height = window.innerHeight
            let canvas = document.querySelector('#name')
            if (!canvas) return
            canvas.width = width;
            canvas.height = height;
            const ctx = canvas.getContext('2d');
            // 新建一个离屏canvas
            const offCanvas = document.createElement('canvas')
            offCanvas.width = width
            offCanvas.height = height
            const ctxOffCanvas = canvas.getContext('2d');
            // 设置canvas字体样式
            ctxOffCanvas.font = '16.5px Arial';
            ctxOffCanvas.strokeStyle = '#FFFFFF';
            ctxOffCanvas.fillStyle = '#000000';
            // texts用来存储显示的名称,重叠的部分就不会放到里面
            const texts = [];
            /**
             * 遍历省份数据,有2个核心功能
             * 1. 将3维坐标转化成2维坐标
             * 2. 后面遍历到的数据,要和前面的数据做碰撞对比,重叠的就不绘制
             * */
            this.map.children.forEach((elem, index) => {
                if (!elem.properties._centroid) return
                // 找到中心点
                const y = -elem.properties._centroid[1]
                const x = elem.properties._centroid[0]
                const z = 4
                // 转化为二维坐标
                const vector = new THREE.Vector3(x, y, z)
                const position = vector.project(this.camera)
                // 构建文本的基本属性:名称,left, top, width, height -> 碰撞对比需要这些坐标数据
                const name = elem.properties.name
                const left = (vector.x + 1) / 2 * width
                const top = -(vector.y - 1) / 2 * height
                const text = {
                    name,
                    left,
                    top,
                    width: ctxOffCanvas.measureText(name).width,
                    height: 16.5
                }
                // 碰撞对比
                let show = true
                for (let i = 0; i < texts.length; i++) {
                    if (
                        (text.left + text.width) < texts[i].left ||
                        (text.top + text.height) < texts[i].top ||
                        (texts[i].left + texts[i].width) < text.left ||
                        (texts[i].top + texts[i].height) < text.top
                    ) {
                        show = true
                    } else {
                        show = false
                        break
                    }
                }
                if (show) {
                    texts.push(text)
                    ctxOffCanvas.strokeText(name, left, top)
                    ctxOffCanvas.fillText(name, left, top)
                }
            })
            // 离屏canvas绘制到canvas中
            ctx.drawImage(offCanvas, 0, 0)
        }
复制代码

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

谢谢阅读

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
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
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
Easter79
Easter79
Lv1
今生可爱与温柔,每一样都不能少。
文章
2.8k
粉丝
5
获赞
1.2k