three.js实现3D地图( 五 )

五、地图下钻
现在除了地图下钻 , 都已经完成了 。地图下钻其实就是把当前地图清空 , 然后再次调用一下 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) } }

three.js实现3D地图

文章插图
下钻
现在已经可以下钻了 , 但是又出现了一个新问题[吐血] 。到省份一级后 , 地图太小了 , 而且位置也没有在中间 。这是由于我们的墨卡托投影 变换的中心点和缩放比例是写死的 , 我们需要让这些参数根据地理数据的不同而生成相对应的值 。
在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差不多刚好 } // 省略代码..... }


推荐阅读