2024-03-20 文末附上了工具类代码(19年前刚入行写的代码了,本身也不是前端,望包容)
前言
因在哔哩哔哩发布过相关视频,收到小伙伴的留言。所以在此讲解,希望能帮到大家
首先附上效果图:

相关文档
绘制步骤-3D立体图形-教程-地图 JS API | 高德地图API (amap.com)
基础 Mesh-立体 Mesh-示例中心-JS API 示例 | 高德地图API (amap.com)
我的开发环境
后端接口是java,前端是vue、js 在这里只做前端代码讲解,后端只是基本数据
正式开始
讲解不周到的地方希望读者指出
1、可能小伙伴们困扰的是如何实现多楼层
由官方给的示例,即上述【相关文档】第二个链接,看到的效果图入下:

只需将此处做修改,即可有悬空效果:

若要实现多楼层,只需将同样的经纬度集合(模型的各个顶点,建筑的话就是四个或多个墙角的经纬度)循环遍历,取不同的高度值即可实现
一层:geometry.vertices.push(x, y, 0);
二层:geometry.vertices.push(x, y, 0 + 1*层高);
三层:geometry.vertices.push(x, y, 0 + 2*层高);
2、在开发过程中,还有一些类似点击、描边的需求,同时还有诸如旋转后,几何体看起来不封闭以及建筑物的地标文字不在模型中央上方的问题
以下附上关键代码,文末附上所有代码
点击事件:(其中有些函数可以在开发文档中找到)

// prism 拾取 map.on('mousedown', function (ev) { var pixel = ev.pixel; var px = new AMap.Pixel(pixel.x, pixel.y); var obj = map.getObject3DByContainerPos(px, [object3Dlayer], false) || {}; // 选中的 object3D 对象,这里为当前 Mesh var object = obj.object; // 被拾取到的对象和拾取射线的交叉点的3D坐标 clickMesh(object, _this); }); / * 点击模型事件处理 * @param obj * @param _this */ function clickMesh(obj, _this) { let pool3d = pool3ds.find(function (one) { return one.mesh === obj; }); pool3ds.forEach(function (pool) { if (pool.change === 1) { pool3ds.slice(pool, 1); updateMeshColor(pool, pool.color, 'reset'); pool3ds.push(pool); } }); if (pool3d) { pool3ds.slice(pool3d, 1); updateMeshColor(pool3d, selectColor, 'change'); pool3ds.push(pool3d); _this.globalVariable.buildingId = pool3d.building.buildingId; _this.showUnitTab(pool3d.fc); } }
讯享网
横线或竖线:
讯享网 function drawVerticalBar(lngLat, endH) { let Line3D = new AMap.Object3D.Line(); let Lgeometry = Line3D.geometry; let origin = map.lngLatToGeodeticCoord(lngLat); Lgeometry.vertices.push(origin.x, origin.y, -endH); Lgeometry.vertexColors.push(9 / 255, 0 / 255, 0 / 255, 0.99); let des = map.lngLatToGeodeticCoord(lngLat); Lgeometry.vertices.push(des.x, des.y, 0); Lgeometry.vertexColors.push(9 / 255, 0 / 255, 0 / 255, 0.99); object3Dlayer.add(Line3D); } function drawLine(bounds, height, color, lineW) { if (bounds == null || bounds.length === 0) { return; } let lineBounds = []; let lineHeight = []; for (let i = 0; i < bounds.length; i++) { lineHeight.push(height); lineBounds.push(bounds[i]); } lineHeight.push(height); lineBounds.push(bounds[0]); let line = new AMap.Object3D.MeshLine({ path: lineBounds, height: lineHeight, color: color, width: lineW }); object3Dlayer.add(line); }
几何体封闭:
... mesh.backOrFront = 'both'; // 关键位置 mesh.transparent = transparent; object3Dlayer.add(mesh); return mesh; ...
地标文字在模型中央上方:(原理:取集合面积的重心)
讯享网 public class LatLngVO { private Double lat; private Double lng; } / * @param vo 重心 * @param voList 多边形经纬度集合 */ private static void calculateCenter(LatLngVO vo, List<LatLngVO> voList) { double pointX = 0.0; double pointY = 0.0; double totalArea = 0.0; LatLngVO p1 = voList.get(1); for (int i = 2; i < voList.size(); i++) { LatLngVO p2 = voList.get(i); double area = calculateArea(voList.get(0), p1, p2); totalArea += area; pointX += (voList.get(0).getLng() + p1.getLng() + p2.getLng()) * area; pointY += (voList.get(0).getLat() + p1.getLat() + p2.getLat()) * area; p1 = p2; } if (totalArea != 0.0) { vo.setLng(pointX / totalArea / 3); vo.setLat(pointY / totalArea / 3); } } private static double calculateArea(LatLngVO p0, LatLngVO p1, LatLngVO p2) { double area = p0.getLng() * p1.getLat() + p1.getLng() * p2.getLat() + p2.getLng() * p0.getLat() - p1.getLng() * p0.getLat() - p2.getLng() * p1.getLat() - p0.getLng() * p2.getLat(); return area / 2 ; }
在附上所有代码之前。我对当时所实现的产品功能和业务场景做大致讲解:
业务场景:
1、有项目(project)、物业(buinding)、单元(unit)三个核心实体,均为一对多关系
2、在开发3d之前。已有2维描边基础,所以有现成的经纬度(建筑群的墙角)
产品功能:
1、按照录入的楼层层高、数量、是否在租等信息展示模型
2、点击模型时,改变颜色或者显示相关弹窗等
重点:
1、构建模型时可能存在楼层悬空后,楼层之间区分不明显,此时可以在中间塞入一个高度接近0且颜色较深的模型模拟出地板的效果
2、模型的点击事件不是很准确(这是必然的,和角度也有一点关系)
代码主要是前端js、vue代码,仅供参考,若有不明确之处,还望多多实验,或评论留言,
附上的代码中 我去掉了一些无关代码,若用心阅读,应该是能理解大致含义的,主要关注方法initAMap即可
<template> <div class="box"> <div id="map-3d"></div> </div> </template> <script> import Vue from 'vue'; import AMap from 'AMap'; import AMapUI from 'AMapUI'; import mapUtil from '../../../utils/mapUtils'; import emptyUtil from '../../../utils/emptyUtils' import $ from 'jquery'; Vue.prototype.$message = Message; let mapType = { weixing : '#map-3d > div.amap-ui-control-container.amap-ui-retina.amap-ui-control-position-rt.amap-ui-control-theme-light > div > form > div.amap-ui-control-layer-base > div.amap-ui-control-layer-base-item.amap-ui-control-layer-base-item-satellite > label', standard : '#map-3d > div.amap-ui-control-container.amap-ui-retina.amap-ui-control-position-rt.amap-ui-control-theme-light > div > form > div.amap-ui-control-layer-base > div.amap-ui-control-layer-base-item.amap-ui-control-layer-base-item-tile > label' }; let map; let object3Dlayer; let pool3ds = []; let texts = []; let sdh = 31; let selectColor = [255 / 255, 245 / 255, 47 / 255, 0.9]; let noUnAreaColor = [131 / 255, 131 / 255, 131 / 255, 0.61]; let hasUnAreaColor = [10 / 255, 183 / 255, 168 / 255, 0.66]; let floorLineC = [9 / 255, 0 / 255, 0 / 255, 0.99]; let landLineC = [6 / 255, 221 / 255, 255 / 255, 1]; // let fc = "rgb(6,221,255)"; let landFaceC = [236 / 255, 245 / 255, 255 / 255, 0.35]; export default { data() { return { mapType : 0, h3d: 30, position: { lng: null, lat: null, }, enums: { }, project: { }, globalVariable: { buildingId: null, unitId: null, building: null, }, buildingList: [], unit: { unitTab: [], unitActive: '', floorActive: '', }, editUnit: { }, map3d: { projectId: null, buildings: [], lat: null, lng: null, landPoints: [], moveTable: [], }, visible: { }, editName: { id: null, name: '', }, center: { lng: null, lat: null, }, loading: {}, rules: {}, copy: { unitList: [], floorNumbers: [], unitId: null, buildingId: null, buildingList: [], }, } }, components: { PriceSetting: PriceSetting, UnitDetail: UnitDetail, }, mounted() { this.initAMap(); }, methods: { echo3dModel() { this.initAMap(); this.visible.removeModelVisible = false; }, remove3dModel() { map.remove(object3Dlayer); pool3ds = []; texts.forEach(t => { t.setMap(null); }); texts = []; this.visible.removeModelVisible = true; }, addOneUnit() { this.resetUnit(); this.visible.addUnitVisible = true; dealWithAddUnit(this); }, cutoverMapType() { if (this.mapType === 1) { $(mapType.standard).click(); this.mapType = 0; return } if (this.mapType === 0) { $(mapType.weixing).click(); this.mapType = 1; } }, copyUnitVue() { if (this.copy.floorNumbers.length === 0) { this.$message.warning('请选择目标楼层'); return; } let buildingId = this.copy.buildingId; let unitId = this.copy.unitId; let floorNumber = this.copy.floorNumbers.join(); this.$axios.get(this.$pmsPath + '/cms/rentalUnit/batchCopy.do?originUnitId=' + unitId + '&targetBuildingId=' + buildingId + '&floorNumbers=' + floorNumber) .then(res => { if (res.code === '0') { this.visible.copyRentalUnitDialog = false; this.$message.success('复制成功'); pool3ds = []; this.initAMap(); let _this = this; setTimeout(function () { _this.showUnitTab(floorNumber); }, 1000); } else { this.$message.error('复制失败 :' + res.msg); } }); }, changeH3d() { this.initAMap(); }, resetPosition() { map.setCenter([this.position.lng, this.position.lat]); }, unitCutover(tab) { this.globalVariable.unitId = tab.label.replace('单元#', ''); this.resetUnit(); this.unitInfo(); }, initAMap() { this.map3d.projectId = this.$route.params.projectId; this.queryAllBuilding(); this.sidebarInfo.projectId = this.map3d.projectId; this.$axios.get(this.$pmsPath + '/cms/project/gd3dMap.do?projectId=' + this.map3d.projectId) .then(res => { if (res.code === '0') { let data = res.data; this.map3d.lat = data.lat; this.map3d.lng = data.lng; this.map3d.landPoints = data.landPoints; this.map3d.buildings = data.buildings; this.sidebarInfo.parkName = data.parkName; this.sidebarInfo.buildingIds = data.buildingIds; this.sidebarInfo.addressDesc = data.addressDesc; sdh = this.h3d; draw(this); } else { this.map3d.lat = 31.; this.map3d.lng = 121.; } }); }, queryAllBuilding() { this.$axios.get(this.$pmsPath + '/cms/project/buildings3d.do?projectId=' + this.map3d.projectId) .then(res => { if (res.code === '0') { this.buildingList = res.data; } }); }, drawBuilding(id, pathList, building) { this.$axios.get(this.$pmsPath + '/cms/project/centerPoint.do?id=' + id) .then(res => { if (res.code === '0') { let center = res.data; this.center.lng = center.lng; this.center.lat = center.lat; initMesh(pathList, building, this); } }); }, } } function dealWithAddUnit(_this) { let building = null; _this.buildingList.forEach(b => { if (b.id === _this.globalVariable.buildingId) { building = b; } }); _this.resetUnit(); _this.editUnit.buildingId = _this.globalVariable.buildingId; _this.globalVariable.unitId = null; _this.editUnit.warehouseType = building.warehouseType; } function draw(_this) { let curr = mapUtil.bd_google_encrypt(_this.map3d.lat, _this.map3d.lng); map = new AMap.Map('map-3d', { viewMode: '3D', // 开启 3D 模式 pitch: 52, rotation: 60, center: [curr.lon, curr.lat], features: ['bg', 'road'], zoom: 17, buildingAnimation:true, mapStyle: "amap://styles/fresh" }); _this.position.lng = curr.lon; _this.position.lat = curr.lat; addPlugin(); map.AmbientLight = new AMap.Lights.AmbientLight([1, 1, 1], 0.5); map.DirectionLight = new AMap.Lights.DirectionLight([1, -1, 2], [1, 1, 1], 0.8); object3Dlayer = new AMap.Object3DLayer(); map.add(object3Dlayer); for (let bx = 0; bx < _this.map3d.buildings.length; bx++) { let building = _this.map3d.buildings[bx]; let coordinates = building.coordinates; // 没有描边 没有层高 没有楼层数 不显示立体 if (coordinates === null || coordinates === '' || building.firstHeight == null || building.floorCount <= 0) { continue; } let cArrays = coordinates.split(';'); let pathList = []; for (let pc = 0; pc < cArrays.length; pc++) { let cArray = cArrays[pc].split(','); pathList.push([cArray[0], cArray[1]]); } _this.drawBuilding(building.buildingId, pathList, building); } drawProjectLand(_this.map3d.landPoints); // 第一次进入默认显示一个物业 if (_this.visible.firstEntry === true) { if (emptyUtil.arrayNotEmpty( _this.sidebarInfo.buildingIds)) { _this.globalVariable.buildingId = _this.sidebarInfo.buildingIds[0]; _this.showUnitTab(null); } _this.visible.firstEntry = false; } // prism 拾取 map.on('mousedown', function (ev) { var pixel = ev.pixel; var px = new AMap.Pixel(pixel.x, pixel.y); var obj = map.getObject3DByContainerPos(px, [object3Dlayer], false) || {}; // 选中的 object3D 对象,这里为当前 Mesh var object = obj.object; // 被拾取到的对象和拾取射线的交叉点的3D坐标 clickMesh(object, _this); }); $('.amap-controlbar-zoom').hide(); $('#map-3d > div.amap-controls > div.amap-maptypecontrol').hide(); } function updateMeshColor(pool, color, action) { if (action === 'change') { pool.change = 1; } if (action === 'reset') { pool.change = 0; } let mesh = pool.mesh; let vertexColors = mesh.geometry.vertexColors; let len = vertexColors.length; for (let i = 0; i <= len / 4; i++) { let r = color[0]; let g = color[1]; let b = color[2]; let a = color[3]; // 不能重新赋值,只允许修改内容 vertexColors.splice(i * 4, 4, r, g, b, a); } mesh.needUpdate = true; mesh.reDraw(); } function drawTextFlag(building, fc, secondH, _this) { let th = (building.firstHeight + ((fc - 1) * secondH)) * sdh; let text = new AMap.Text({ text: emptyUtil.isEmpty(building.anotherName) ? ('#' + building.buildingId) : building.anotherName, verticalAlign: 'bottom', position: [_this.center.lng, _this.center.lat], height: th, style: { 'background-color': 'transparent', '-webkit-text-stroke': 'white', '-webkit-text-stroke-width': '0.4px', 'text-align': 'center', 'border': 'none', 'color': 'white', 'font-size': '14px', } }); text.setMap(map); texts.push(text); } function initMesh(paths, building, _this) { let fc = building.floorCount; let secondH; if (building.secondAndMoreHeight == null) { secondH = 3; } else { secondH = building.secondAndMoreHeight; } // 物业标记 drawTextFlag(building, fc, secondH, _this); for (let fci = 1; fci <= fc; fci++) { drawFloor3dModel(paths, building, fci, secondH); } } function drawMiddleLayer(bounds, color, startH, endH, transparent) { let mesh = new AMap.Object3D.Mesh(); let geometry = mesh.geometry; let vertices = geometry.vertices; let vertexColors = geometry.vertexColors; let faces = geometry.faces; let vertexLength = bounds.length * 2; let verArr = []; bounds.forEach(function (lngLat, index) { let g20 = map.lngLatToGeodeticCoord(lngLat); verArr.push([g20.x, g20.y]); // 构建顶点-底面顶点 vertices.push(g20.x, g20.y, -startH); // 构建顶点-顶面顶点 vertices.push(g20.x, g20.y, -endH); vertexColors.push.apply(vertexColors, color); vertexColors.push.apply(vertexColors, color); let bottomIndex = index * 2; let topIndex = bottomIndex + 1; let nextBottomIndex = (bottomIndex + 2) % vertexLength; let nextTopIndex = (bottomIndex + 3) % vertexLength; //侧面三角形1 faces.push(bottomIndex, topIndex, nextTopIndex); //侧面三角形2 faces.push(bottomIndex, nextTopIndex, nextBottomIndex); drawVerticalBar(lngLat, endH); }); // 物业描边 不使用黑色 if (color === landFaceC) { drawLine(bounds, endH, landLineC, 1); } else { drawLine(bounds, endH, floorLineC, 1); } // 设置顶面,根据顶点拆分三角形 let triangles = AMap.GeometryUtil.triangulateShape(verArr); for (let v = 0; v < triangles.length; v += 3) { let a = triangles[v]; let b = triangles[v + 2]; let c = triangles[v + 1]; faces.push(a * 2 + 1, b * 2 + 1, c * 2 + 1); } mesh.backOrFront = 'both'; mesh.transparent = transparent; object3Dlayer.add(mesh); return mesh; } function drawProjectLand(landPoints) { if (emptyUtil.arrayIsEmpty(landPoints)) { return; } let landBounds = landPoints.map(function (p) { return new AMap.LngLat(p.lng, p.lat); }); drawMiddleLayer(landBounds, landFaceC, 0, 1, true); } / * 画具体楼层3d模型 */ function drawFloor3dModel(path, building, fci, secondH) { let bounds = path.map(function (p) { return new AMap.LngLat(p[0], p[1]); }); if (fci === 1) { // 最底层边线 drawLine(bounds, 0, floorLineC, 1); } let unit = null; if (building.unitList != null && building.unitList.length > 0) { unit = building.unitList.find(function (unitOne) { return unitOne.floorNumber === fci && unitOne.unoccupiedArea != null && unitOne.unoccupiedArea > 0; }); } let startH = 0; let endH = 0; let fh = building.firstHeight * sdh; if (fci === 1) { endH = fh; } else { startH = fh + ((fci - 2) * sdh * secondH); endH = fh + ((fci - 1) * sdh * secondH); } let color = noUnAreaColor; // 当前有空置面积 if (unit != null) { color = hasUnAreaColor; drawMiddleLayer(bounds, hasUnAreaColor, startH, startH + 5, false); } else { drawMiddleLayer(bounds, noUnAreaColor, startH, startH + 5, false); } let mesh = drawMiddleLayer(bounds, color, startH, endH, true); let pool3d = {}; pool3d.mesh = mesh; pool3d.change = 0; // 优化物业太多时 重置颜色很慢 pool3d.color = color; pool3d.fc = fci; pool3d.building = building; pool3ds.push(pool3d); } / * 点击具体单元改变模型颜色 * @param buildingId * @param fc */ function editUnitColor(buildingId, fc) { let pool3d = pool3ds.find(function (one) { return one.building.buildingId === buildingId && one.fc === fc; }); pool3ds.forEach(function (pool) { if (pool.change === 1) { pool3ds.slice(pool, 1); updateMeshColor(pool, pool.color, 'reset'); pool3ds.push(pool); } }); if (pool3d) { pool3ds.slice(pool3d, 1); updateMeshColor(pool3d, selectColor, 'change'); pool3ds.push(pool3d); } } / * 点击模型事件处理 * @param obj * @param _this */ function clickMesh(obj, _this) { let pool3d = pool3ds.find(function (one) { return one.mesh === obj; }); pool3ds.forEach(function (pool) { if (pool.change === 1) { pool3ds.slice(pool, 1); updateMeshColor(pool, pool.color, 'reset'); pool3ds.push(pool); } }); if (pool3d) { pool3ds.slice(pool3d, 1); updateMeshColor(pool3d, selectColor, 'change'); pool3ds.push(pool3d); _this.globalVariable.buildingId = pool3d.building.buildingId; _this.showUnitTab(pool3d.fc); } } function drawLine(bounds, height, color, lineW) { if (bounds == null || bounds.length === 0) { return; } let lineBounds = []; let lineHeight = []; for (let i = 0; i < bounds.length; i++) { lineHeight.push(height); lineBounds.push(bounds[i]); } lineHeight.push(height); lineBounds.push(bounds[0]); let line = new AMap.Object3D.MeshLine({ path: lineBounds, height: lineHeight, color: color, width: lineW }); object3Dlayer.add(line); } function drawVerticalBar(lngLat, endH) { let Line3D = new AMap.Object3D.Line(); let Lgeometry = Line3D.geometry; let origin = map.lngLatToGeodeticCoord(lngLat); Lgeometry.vertices.push(origin.x, origin.y, -endH); Lgeometry.vertexColors.push(9 / 255, 0 / 255, 0 / 255, 0.99); let des = map.lngLatToGeodeticCoord(lngLat); Lgeometry.vertices.push(des.x, des.y, 0); Lgeometry.vertexColors.push(9 / 255, 0 / 255, 0 / 255, 0.99); object3Dlayer.add(Line3D); } function addPlugin() { map.addControl(new AMap.ControlBar({ position: { bottom: '150px', right: '0px', } })); //加载BasicControl,loadUI的路径参数为模块名中 'ui/' 之后的部分 AMapUI.loadUI(['control/BasicControl'], function (BasicControl) { //图层切换控件 map.addControl(new BasicControl.LayerSwitcher({ position: 'rt' //right top,右上角 })); //添加一个缩放控件 map.addControl(new BasicControl.Zoom({ position: 'rm' })); }); $('.amap-controlbar-zoom').hide(); $('#map-3d > div.amap-controls > div.amap-maptypecontrol').hide(); } </script>
MapUtil
讯享网let mapUtils = { bd_google_encrypt(bd_lat, bd_lon) { var x_pi = 3. * 3000.0 / 180.0; var point = {}; var x = bd_lon - 0.0065; var y = bd_lat - 0.006; var z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * x_pi); var theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * x_pi); point.lon = z * Math.cos(theta); point.lat = z * Math.sin(theta); return point; }, avg(array) { var len = array.length; var sum = 0; for (var i = 0; i < len; i++) { sum = sum + Number(array[i]); } return (sum / len); } }; export default mapUtils;
EmptyUtil
let emptyUtils = { isNotEmpty(value) { return value != undefined && value != null && value != ''; }, isEmpty(value) { return !emptyUtils.isNotEmpty(value); }, arrayIsEmpty(array) { return array == undefined || array == null || array.length <= 0; }, arrayNotEmpty(array) { return !emptyUtils.arrayIsEmpty(array); } } export default emptyUtils;
vue.config.js
讯享网... ... module.exports = { ... ... configureWebpack: { externals: { AMap: "window.AMap", AMapUI: "window.AMapUI" } }, };
下载完整代码:https://download.csdn.net/download/hainiugen/

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/116791.html