日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請(qǐng)做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

地圖下鉆是前端開發(fā)中常見的開發(fā)需求。通常會(huì)使用高德、百度等第三方地圖實(shí)現(xiàn),不過這些都不是3d的。echarts倒是提供了map3D,以及常用的點(diǎn)位、飛線等功能,就是有一些小bug[淚奔],而且如果領(lǐng)導(dǎo)比較可愛,提一些奇奇怪怪的需求,可能就不好搞了……

這篇文章我會(huì)用three.js實(shí)現(xiàn)一個(gè)geojson下鉆地圖。

地圖預(yù)覽

一、搭建環(huán)境

我這里用parcel搭建一個(gè)簡易的開發(fā)環(huán)境,安裝依賴如下:

{ "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" } }二、創(chuàng)建場(chǎng)景、相機(jī)、渲染器以及地圖import * as THREE from 'three' class Map3D { constructor() { this.scene = undefined // 場(chǎng)景 this.camera = undefined // 相機(jī) this.renderer = undefined // 渲染器 this.init() } init() { // 創(chuàng)建場(chǎng)景 this.scene = new THREE.Scene() // 創(chuàng)建相機(jī) this.setCamera() // 創(chuàng)建渲染器 this.setRender() // 渲染函數(shù) this.render() } /** * 創(chuàng)建相機(jī) */ setCamera() { // PerspectiveCamera(角度,長寬比,近端面,遠(yuǎn)端面) —— 透視相機(jī) this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ) // 設(shè)置相機(jī)位置 this.camera.position.set(0, 0, 120) // 把相機(jī)添加到場(chǎng)景中 this.scene.add(this.camera) } /** * 創(chuàng)建渲染器 */ setRender() { this.renderer = new THREE.WebGLRenderer() // 渲染器尺寸 this.renderer.setSize(window.innerWidth, window.innerHeight) //設(shè)置背景顏色 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()

場(chǎng)景、相機(jī)、渲染器是threejs中必不可少的要素。以上代碼運(yùn)行起來后可以看到屏幕一片黑,審查元素是一個(gè)canvas占據(jù)了窗口。

啥也沒有

接下來需要geojson數(shù)據(jù)了,阿里的datav免費(fèi)提供區(qū)級(jí)以上的數(shù)據(jù):https://datav.aliyun.com/portal/school/atlas/area_selector

class 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大概是下圖這樣的數(shù)據(jù)格式:

geojson

然后,我們初始化一個(gè)地圖 當(dāng)然,咱們拿到的json數(shù)據(jù)中的所有坐標(biāo)都是經(jīng)緯度坐標(biāo),是不能直接在我們的threejs項(xiàng)目中使用的。需要 “墨卡托投影轉(zhuǎn)換”把經(jīng)緯度轉(zhuǎn)換成畫布中的坐標(biāo)。在這里,我們使用現(xiàn)成的工具——d3中的墨卡托投影轉(zhuǎn)換工具

import * as d3 from 'd3-geo' class Map3D { ...... async loadData(adcode) { // 獲取geojson數(shù)據(jù) this.geojson = await this.getGeoJson(adcode) // 墨卡托投影轉(zhuǎn)換。將中心點(diǎn)設(shè)置成經(jīng)緯度為 104.0, 37.5 的地點(diǎn),且不平移 this.projection = d3 .geoMercator() .center([104.0, 37.5]) .translate([0, 0]) } }

接著就可以創(chuàng)建地圖了。

創(chuàng)建地圖的思路:以中國地圖為例,創(chuàng)建一個(gè)Object3D對(duì)象,作為整個(gè)中國地圖。再創(chuàng)建N個(gè)Object3D子對(duì)象,每個(gè)子對(duì)象都是一個(gè)省份,再將這些子對(duì)象add到中國地圖這個(gè)父Object3D對(duì)象上。

地圖結(jié)構(gòu)

創(chuàng)建地圖后的完整代碼:

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 // 場(chǎng)景 this.camera = undefined // 相機(jī) this.renderer = undefined // 渲染器 this.geojson = undefined // 地圖json數(shù)據(jù) this.init() } init() { // 創(chuàng)建場(chǎng)景 this.scene = new THREE.Scene() // 創(chuàng)建相機(jī) this.setCamera() // 創(chuàng)建渲染器 this.setRender() // 渲染函數(shù) this.render() // 加載數(shù)據(jù) this.loadData() } /** * 創(chuàng)建相機(jī) */ setCamera() { // PerspectiveCamera(角度,長寬比,近端面,遠(yuǎn)端面) —— 透視相機(jī) this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ) // 設(shè)置相機(jī)位置 this.camera.position.set(0, 0, 120) // 把相機(jī)添加到場(chǎng)景中 this.scene.add(this.camera) } /** * 創(chuàng)建渲染器 */ setRender() { this.renderer = new THREE.WebGLRenderer() // 渲染器尺寸 this.renderer.setSize(window.innerWidth, window.innerHeight) //設(shè)置背景顏色 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數(shù)據(jù) this.geojson = await this.getGeoJson(adcode) // 創(chuàng)建墨卡托投影 this.projection = d3 .geoMercator() .center([104.0, 37.5]) .translate([0, 0]) // Object3D是Three.js中大部分對(duì)象的基類,提供了一系列的屬性和方法來對(duì)三維空間中的物體進(jìn)行操縱。 // 初始化一個(gè)地圖 this.map = new THREE.Object3D(); this.geojson.features.forEach(elem => { const area = new THREE.Object3D() // 坐標(biāo)系數(shù)組(為什么是數(shù)組,因?yàn)橛械貐^(qū)不止一個(gè)幾何體,比如河北被北京分開了,比如舟山群島) const coordinates = elem.geometry.coordinates const type = elem.geometry.type // 定義一個(gè)畫幾何體的方法 const drawPolygon = (polygon) => { // Shape(形狀)。使用路徑以及可選的孔洞來定義一個(gè)二維形狀平面。 它可以和ExtrudeGeometry、ShapeGeometry一起使用,獲取點(diǎn),或者獲取三角面。 const shape = new THREE.Shape() // 存放的點(diǎn)位,最后需要用THREE.Line將點(diǎn)位構(gòu)成一條線,也就是地圖上區(qū)域間的邊界線 // 為什么兩個(gè)數(shù)組,因?yàn)樾枰S地圖的兩面都畫線,且它們的z坐標(biāo)不同 let points1 = []; let points2 = []; for (let i = 0; i < polygon.length; i++) { // 將經(jīng)緯度通過墨卡托投影轉(zhuǎn)換成threejs中的坐標(biāo) const [x, y] = this.projection(polygon[i]); // 畫二維形狀 if (i === 0) { shape.moveTo(x, -y); } shape.l.NETo(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, }); /** * 基礎(chǔ)材質(zhì) */ // 正反兩面的材質(zhì) const material1 = new THREE.MeshBasicMaterial({ color: MATERIAL_COLOR1, }); // 側(cè)邊材質(zhì) const material2 = new THREE.MeshBasicMaterial({ color: MATERIAL_COLOR2, }); // 生成一個(gè)幾何物體(如果是中國地圖,那么每一個(gè)mesh就是一個(gè)省份幾何體) 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); }); } // 把區(qū)域添加到地圖中 this.map.add(area); }) // 把地圖添加到場(chǎng)景中 this.scene.add(this.map) } } const map = new Map3D()

簡單地圖

這時(shí),已經(jīng)生成一個(gè)完整的地圖,但是當(dāng)我們?cè)囍ソ换r(shí)還不能旋轉(zhuǎn),只需要添加一個(gè)控制器

// 引入構(gòu)造器 import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' init() { this.setControls() } setControls() { this.controls = new OrbitControls(this.camera, this.renderer.domElement) // 太靈活了,來個(gè)阻尼 this.controls.enableDamping = true; this.controls.dampingFactor = 0.1; }

controls

好了,現(xiàn)在就可以想看哪兒就看哪兒了。

三、當(dāng)鼠標(biāo)移入地圖時(shí)讓對(duì)應(yīng)的地區(qū)高亮

Raycaster —— 光線投射Raycaster
文檔鏈接:https://threejs.org/docs/index.html?q=Raycaster#api/zh/core/Raycaster

Raycaster用于進(jìn)行raycasting(光線投射)。 光線投射用于進(jìn)行鼠標(biāo)拾取(在三維空間中計(jì)算出鼠標(biāo)移過了什么物體)。

這個(gè)類有兩個(gè)方法,
第一個(gè)setFromCamera(coords, camera)方法,它接收兩個(gè)參數(shù):
coords —— 在標(biāo)準(zhǔn)化設(shè)備坐標(biāo)中鼠標(biāo)的二維坐標(biāo) —— X分量與Y分量應(yīng)當(dāng)在-1到1之間。
camera —— 射線所來源的攝像機(jī)。
通過這個(gè)方法可以更新射線。

第二個(gè)intersectObjects: 檢測(cè)所有在射線與這些物體之間,包括或不包括后代的相交部分。返回結(jié)果時(shí),相交部分將按距離進(jìn)行排序,最近的位于第一個(gè))。

我們可以通過監(jiān)聽鼠標(biāo)事件,實(shí)時(shí)更新鼠標(biāo)的坐標(biāo),同時(shí)實(shí)時(shí)在渲染函數(shù)中更新射線,然后通過intersectObjects方法查找當(dāng)前鼠標(biāo)移過的物體。

// 以下是新添加的代碼 init() { // 創(chuàng)建場(chǎng)景 this.scene = new THREE.Scene() // 創(chuàng)建相機(jī) this.setCamera() // 創(chuàng)建渲染器 this.setRender() // 創(chuàng)建控制器 this.setControls() // 光線投射 this.setRaycaster() // 加載數(shù)據(jù) this.loadData() // 渲染函數(shù) this.render() } setRaycaster() { this.raycaster = new THREE.Raycaster(); this.mouse = new THREE.Vector2(); const onMouse = (event) => { // 將鼠標(biāo)位置歸一化為設(shè)備坐標(biāo)。x 和 y 方向的取值范圍是 (-1 to +1) // threejs的三維坐標(biāo)系是中間為原點(diǎn),鼠標(biāo)事件的獲得的坐標(biāo)是左上角為原點(diǎn)。因此需要在這里轉(zhuǎn)換 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存在,將材質(zhì)顏色還原 if (this.lastPick) { this.lastPick.object.material[0].color.set(MATERIAL_COLOR1); this.lastPick.object.material[1].color.set(MATERIAL_COLOR2); } // 置空 this.lastPick = null; // 查詢當(dāng)前鼠標(biāo)移動(dòng)所產(chǎn)生的射線與物體的焦點(diǎn) // 有兩個(gè)material的就是我們要找的對(duì)象 this.lastPick = intersects.find( (item) => item.object.material && item.object.material.length === 2 ); // 找到后把顏色換成一個(gè)鮮艷的綠色 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)) }

高亮

四、還差一個(gè)tooltip

引入 css2DRenderer 和 CSS2DObject,創(chuàng)建一個(gè)2D渲染器,用2D渲染器生成一個(gè)tooltip。在此之前,需要在 loadData方法創(chuàng)建area時(shí)把地區(qū)屬性添加到Mesh對(duì)象上。確保lastPick對(duì)象上能取到地域名稱。

// 把地區(qū)屬性存到area對(duì)象中 area.properties = elem.properties

把地區(qū)屬性存到Mash對(duì)象中

// 引入CSS2DObject, CSS2DRenderer import { CSS2DObject, CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer' class Map3D { setRender() { ...... // CSS2DRenderer 創(chuàng)建的是html的div元素 // 這里將div設(shè)置成絕對(duì)定位,蓋住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("div"); 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中國地圖

此時(shí)的完整代碼:

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 // 場(chǎng)景 this.camera = undefined // 相機(jī) this.renderer = undefined // 渲染器 this.css2dRenderer = undefined // html渲染器 this.geojson = undefined // 地圖json數(shù)據(jù) this.init() } init() { // 創(chuàng)建場(chǎng)景 this.scene = new THREE.Scene() // 創(chuàng)建相機(jī) this.setCamera() // 創(chuàng)建渲染器 this.setRender() // 創(chuàng)建控制器 this.setControls() // 光線投射 this.setRaycaster() // 加載數(shù)據(jù) this.loadData() // 渲染函數(shù) this.render() } /** * 創(chuàng)建相機(jī) */ setCamera() { // PerspectiveCamera(角度,長寬比,近端面,遠(yuǎn)端面) —— 透視相機(jī) this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ) // 設(shè)置相機(jī)位置 this.camera.position.set(0, 0, 120) // 把相機(jī)添加到場(chǎng)景中 this.scene.add(this.camera) } /** * 創(chuàng)建渲染器 */ setRender() { this.renderer = new THREE.WebGLRenderer() // 渲染器尺寸 this.renderer.setSize(window.innerWidth, window.innerHeight) //設(shè)置背景顏色 this.renderer.setClearColor(0x000000) // 將渲染器追加到dom中 document.body.appendChild(this.renderer.domElement) // CSS2DRenderer 創(chuàng)建的是html的div元素 // 這里將div設(shè)置成絕對(duì)定位,蓋住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) => { // 將鼠標(biāo)位置歸一化為設(shè)備坐標(biāo)。x 和 y 方向的取值范圍是 (-1 to +1) // threejs的三維坐標(biāo)系是中間為原點(diǎn),鼠標(biāo)事件的獲得的坐標(biāo)是左上角為原點(diǎn)。因此需要在這里轉(zhuǎn)換 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("div"); 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存在,將材質(zhì)顏色還原 if (this.lastPick) { this.lastPick.object.material[0].color.set(MATERIAL_COLOR1); this.lastPick.object.material[1].color.set(MATERIAL_COLOR2); } // 置空 this.lastPick = null; // 查詢當(dāng)前鼠標(biāo)移動(dòng)所產(chǎn)生的射線與物體的焦點(diǎn) // 有兩個(gè)material的就是我們要找的對(duì)象 this.lastPick = intersects.find( (item) => item.object.material && item.object.material.length === 2 ); // 找到后把顏色換成一個(gè)鮮艷的綠色 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) // 太靈活了,來個(gè)阻尼 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數(shù)據(jù) this.geojson = await this.getGeoJson(adcode) // 創(chuàng)建墨卡托投影 this.projection = d3 .geoMercator() .center([104.0, 37.5]) .translate([0, 0]) // Object3D是Three.js中大部分對(duì)象的基類,提供了一系列的屬性和方法來對(duì)三維空間中的物體進(jìn)行操縱。 // 初始化一個(gè)地圖 this.map = new THREE.Object3D(); this.geojson.features.forEach(elem => { const area = new THREE.Object3D() // 坐標(biāo)系數(shù)組(為什么是數(shù)組,因?yàn)橛械貐^(qū)不止一個(gè)幾何體,比如河北被北京分開了,比如舟山群島) const coordinates = elem.geometry.coordinates const type = elem.geometry.type // 定義一個(gè)畫幾何體的方法 const drawPolygon = (polygon) => { // Shape(形狀)。使用路徑以及可選的孔洞來定義一個(gè)二維形狀平面。 它可以和ExtrudeGeometry、ShapeGeometry一起使用,獲取點(diǎn),或者獲取三角面。 const shape = new THREE.Shape() // 存放的點(diǎn)位,最后需要用THREE.Line將點(diǎn)位構(gòu)成一條線,也就是地圖上區(qū)域間的邊界線 // 為什么兩個(gè)數(shù)組,因?yàn)樾枰S地圖的兩面都畫線,且它們的z坐標(biāo)不同 let points1 = []; let points2 = []; for (let i = 0; i < polygon.length; i++) { // 將經(jīng)緯度通過墨卡托投影轉(zhuǎn)換成threejs中的坐標(biāo) 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, }); /** * 基礎(chǔ)材質(zhì) */ // 正反兩面的材質(zhì) const material1 = new THREE.MeshBasicMaterial({ color: MATERIAL_COLOR1, }); // 側(cè)邊材質(zhì) const material2 = new THREE.MeshBasicMaterial({ color: MATERIAL_COLOR2, }); // 生成一個(gè)幾何物體(如果是中國地圖,那么每一個(gè)mesh就是一個(gè)省份幾何體) 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); // 把地區(qū)屬性存到area對(duì)象中 area.properties = elem.properties } // type可能是MultiPolygon 也可能是Polygon if (type === "MultiPolygon") { coordinates.forEach((multiPolygon) => { multiPolygon.forEach((polygon) => { drawPolygon(polygon); }); }); } else { coordinates.forEach((polygon) => { drawPolygon(polygon); }); } // 把區(qū)域添加到地圖中 this.map.add(area); }) // 把地圖添加到場(chǎng)景中 this.scene.add(this.map) } } const map = new Map3D()五、地圖下鉆

現(xiàn)在除了地圖下鉆,都已經(jīng)完成了。地圖下鉆其實(shí)就是把當(dāng)前地圖清空,然后再次調(diào)用一下 loadData 方法,傳入adcode就可以創(chuàng)建對(duì)應(yīng)地區(qū)的3D地圖了。

思路非常簡單,先綁定點(diǎn)擊事件,這里就不需要光線投射了,因?yàn)橐呀?jīng)監(jiān)聽mousever事件了,并且數(shù)據(jù)已經(jīng)存在this.lastPick這個(gè)變量中了。只需要在監(jiān)聽點(diǎn)擊時(shí)獲取選中的lastPick對(duì)象就可以了。

然后調(diào)用this.loadData(areaId),不過...在調(diào)用loadData方法前需要將創(chuàng)建的地圖清空,并且釋放幾何體和材質(zhì)對(duì)象,防止內(nèi)存泄露。

理清思路后開始動(dòng)手。

首先綁定點(diǎn)擊事件。我們?cè)谡{(diào)用點(diǎn)擊事件時(shí),例如高德地圖、echarts,會(huì)以 obj.on('click', callback)的形式調(diào)用,這樣就不會(huì)局限于click事件了,雙擊事件以及其它的事件都可以監(jiān)聽和移除,那我們也試著這么做一個(gè)。在Map3D類中創(chuàng)建一個(gè)on 監(jiān)聽事件的方法和一個(gè)off 移除事件的方法。

class Map3D{ constructor() { // 監(jiān)聽回調(diào)事件存儲(chǔ)區(qū) this.callbackStack = new Map(); } // 省略代碼...... // 添加監(jiān)聽事件 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)); } // 移除監(jiān)聽事件 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對(duì)象 console.log(data) // 區(qū)域編碼 console.log(data.object.parent.properties.adcode) }

在上面的 listener 回調(diào)方法中打印可以獲取到當(dāng)前點(diǎn)擊區(qū)域。
先忍住調(diào)用loadData()方法,在此之前,要先抹掉之前一番操作搞出來的地圖。

在Map3D類中再創(chuàng)建一個(gè)dispose方法,用來移除地圖以及釋放內(nèi)存

class Map3D { // 省略代碼...... dispose (o) { // 可以遍歷該父場(chǎng)景中的所有子物體來執(zhí)行回調(diào)函數(shù) 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) { // 區(qū)域編碼 const adcode = data.object.parent.properties.adcode if(adcode) { map.dispose(map.map) map.loadData(adcode) } }

下鉆

現(xiàn)在已經(jīng)可以下鉆了,但是又出現(xiàn)了一個(gè)新問題[吐血]。到省份一級(jí)后,地圖太小了,而且位置也沒有在中間。這是由于我們的墨卡托投影 變換的中心點(diǎn)和縮放比例是寫死的,我們需要讓這些參數(shù)根據(jù)地理數(shù)據(jù)的不同而生成相對(duì)應(yīng)的值。

在geojson中,coordinates數(shù)組中的坐標(biāo)就是這塊區(qū)域的邊界線上的點(diǎn),以浙江省為例,只要找出浙江省邊界線上點(diǎn)位的最大橫向坐標(biāo)(maxX)和最小橫向坐標(biāo)(minX),它們的和 / 2 就能得到X軸上的中心點(diǎn)。同理Y軸中心點(diǎn)也是如此。

縮放倍數(shù)只需要根據(jù)畫布的寬與浙江省橫向長度比值和畫布的高與浙江省縱向長度比值中取一個(gè)最小值再乘以一個(gè)系數(shù)(待定)。

開始動(dòng)手,在Map3D類中添加getCenter方法:

class Map3D{ // 省略代碼..... // 獲取中心點(diǎn)和縮放倍數(shù) 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數(shù)據(jù) this.geojson = await this.getGeoJson(adcode) const { center, scale } = this.getCenter() // 創(chuàng)建墨卡托投影 this.projection = d3 .geoMercator() .center(center) .translate([0, 0]) .scale(scale * 7) // 根據(jù)實(shí)測(cè),系數(shù)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() { // 監(jiān)聽回調(diào)事件存儲(chǔ)區(qū) this.callbackStack = new Map(); this.scene = undefined // 場(chǎng)景 this.camera = undefined // 相機(jī) this.renderer = undefined // 渲染器 this.css2dRenderer = undefined // html渲染器 this.geojson = undefined // 地圖json數(shù)據(jù) this.init() } init() { // 創(chuàng)建場(chǎng)景 this.scene = new THREE.Scene() // 創(chuàng)建相機(jī) this.setCamera() // 創(chuàng)建渲染器 this.setRender() // 創(chuàng)建控制器 this.setControls() // 光線投射 this.setRaycaster() // 加載數(shù)據(jù) this.loadData() // 渲染函數(shù) this.render() } /** * 創(chuàng)建相機(jī) */ setCamera() { // PerspectiveCamera(角度,長寬比,近端面,遠(yuǎn)端面) —— 透視相機(jī) this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ) // 設(shè)置相機(jī)位置 this.camera.position.set(0, 0, 120) // 把相機(jī)添加到場(chǎng)景中 this.scene.add(this.camera) } /** * 創(chuàng)建渲染器 */ setRender() { this.renderer = new THREE.WebGLRenderer() // 渲染器尺寸 this.renderer.setSize(window.innerWidth, window.innerHeight) //設(shè)置背景顏色 this.renderer.setClearColor(0x000000) // 將渲染器追加到dom中 document.body.appendChild(this.renderer.domElement) // CSS2DRenderer 創(chuàng)建的是html的div元素 // 這里將div設(shè)置成絕對(duì)定位,蓋住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) => { // 將鼠標(biāo)位置歸一化為設(shè)備坐標(biāo)。x 和 y 方向的取值范圍是 (-1 to +1) // threejs的三維坐標(biāo)系是中間為原點(diǎn),鼠標(biāo)事件的獲得的坐標(biāo)是左上角為原點(diǎn)。因此需要在這里轉(zhuǎn)換 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("div"); 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存在,將材質(zhì)顏色還原 if (this.lastPick) { this.lastPick.object.material[0].color.set(MATERIAL_COLOR1); this.lastPick.object.material[1].color.set(MATERIAL_COLOR2); } // 置空 this.lastPick = null; // 查詢當(dāng)前鼠標(biāo)移動(dòng)所產(chǎn)生的射線與物體的焦點(diǎn) // 有兩個(gè)material的就是我們要找的對(duì)象 this.lastPick = intersects.find( (item) => item.object.material && item.object.material.length === 2 ); // 找到后把顏色換成一個(gè)鮮艷的綠色 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) // 太靈活了,來個(gè)阻尼 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()) } // 獲取中心點(diǎn)和縮放倍數(shù) 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數(shù)據(jù) this.geojson = await this.getGeoJson(adcode) const { center, scale } = this.getCenter() // 創(chuàng)建墨卡托投影 this.projection = d3 .geoMercator() .center(center) .translate([0, 0]) .scale(scale * 7) // 根據(jù)實(shí)測(cè),系數(shù)7差不多剛好 // Object3D是Three.js中大部分對(duì)象的基類,提供了一系列的屬性和方法來對(duì)三維空間中的物體進(jìn)行操縱。 // 初始化一個(gè)地圖 this.map = new THREE.Object3D(); this.geojson.features.forEach(elem => { const area = new THREE.Object3D() // 坐標(biāo)系數(shù)組(為什么是數(shù)組,因?yàn)橛械貐^(qū)不止一個(gè)幾何體,比如河北被北京分開了,比如舟山群島) const coordinates = elem.geometry.coordinates const type = elem.geometry.type // 定義一個(gè)畫幾何體的方法 const drawPolygon = (polygon) => { // Shape(形狀)。使用路徑以及可選的孔洞來定義一個(gè)二維形狀平面。 它可以和ExtrudeGeometry、ShapeGeometry一起使用,獲取點(diǎn),或者獲取三角面。 const shape = new THREE.Shape() // 存放的點(diǎn)位,最后需要用THREE.Line將點(diǎn)位構(gòu)成一條線,也就是地圖上區(qū)域間的邊界線 // 為什么兩個(gè)數(shù)組,因?yàn)樾枰S地圖的兩面都畫線,且它們的z坐標(biāo)不同 let points1 = []; let points2 = []; for (let i = 0; i < polygon.length; i++) { // 將經(jīng)緯度通過墨卡托投影轉(zhuǎn)換成threejs中的坐標(biāo) 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, }); /** * 基礎(chǔ)材質(zhì) */ // 正反兩面的材質(zhì) const material1 = new THREE.MeshBasicMaterial({ color: MATERIAL_COLOR1, }); // 側(cè)邊材質(zhì) const material2 = new THREE.MeshBasicMaterial({ color: MATERIAL_COLOR2, }); // 生成一個(gè)幾何物體(如果是中國地圖,那么每一個(gè)mesh就是一個(gè)省份幾何體) 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); // 把地區(qū)屬性存到area對(duì)象中 area.properties = elem.properties } // type可能是MultiPolygon 也可能是Polygon if (type === "MultiPolygon") { coordinates.forEach((multiPolygon) => { multiPolygon.forEach((polygon) => { drawPolygon(polygon); }); }); } else { coordinates.forEach((polygon) => { drawPolygon(polygon); }); } // 把區(qū)域添加到地圖中 this.map.add(area); }) // 把地圖添加到場(chǎng)景中 this.scene.add(this.map) } dispose (o) { // 可以遍歷該父場(chǎng)景中的所有子物體來執(zhí)行回調(diào)函數(shù) 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) } // 添加監(jiān)聽事件 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)); } // 移除監(jiān)聽事件 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) { // 區(qū)域編碼 const adcode = data.object.parent.properties.adcode if(adcode) { map.dispose(map.map) map.loadData(adcode) } }

分享到:
標(biāo)簽:js
用戶無頭像

網(wǎng)友整理

注冊(cè)時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊(cè)賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績?cè)u(píng)定2018-06-03

通用課目體育訓(xùn)練成績?cè)u(píng)定