Threejs繪製地圖(geojson)

目前接觸了一些室內地圖的開發工做,二維的、三維的,數據源基本都是採用geojson格式git

基於geojson的地圖繪製目前已經有比較成熟的框架和解決方案了。github

可是今天咱們仍是要在Threejs裏來簡單實現一下三維數據的展現。npm

代碼地址 預覽地址 json

主要實現了2個功能canvas

  • 三維地圖展現
  • POI信息顯示

數據採集

首先咱們須要一份中國省份的輪廓數據數組

geojson代碼截圖

在這份數據咱們須要的字段有瀏覽器

  • 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 來繪製地圖bash

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

複製代碼

座標轉化

咱們的座標數據是經緯度座標,咱們須要把它轉化成平面座標 這裏用到了 d3-geo 的座標轉化方法框架

多面繪製

注意這裏的類型是 MultiPolygon(多面),咱們的座標點是嵌套在多層數組裏面的。dom

由於咱們的數據中,

有的省份輪廓是閉合的

有的省份是多個部分組成的

代碼實現

咱們的模型分紅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;

謝謝閱讀

相關文章
相關標籤/搜索