maptalks + three.js 建设3D城市

2,904 阅读2分钟

最近有个可视化大屏渲染3d城市建筑的需求,看起来很酷炫,最终使用maptalks+three.js初步实现。效果图如下

分析需求:加载地图,在地图上渲染3d建筑物,根据经纬度添加标记物。

看起来挺简单明确,但对于没搞过3d,没搞过gis的我来说,每一步都举步维艰。刚开始并没有找到maptalks这个解决方案,很难下手,发现了maptalks,恰巧完美的解决了需求。

首先的首先安装依赖,并引入

1.加载地图

maptalks使用起来很简单,文档很清晰,国内访问速度可能会有些慢:maptalks.org/

//第一个参数为div容器id 
var map = new maptalks.Map("map", {      center: [117.12013, 36.652066], //地图中心点      zoom: 15, //缩放      pitch: 70, //倾斜度      //加载地图底图,可选择地图来源,可选择亮色或暗色      baseLayer: new maptalks.TileLayer("tile", {        urlTemplate:          "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",        subdomains: ["a", "b", "c", "d"],        attribution:          '&copy; <a href="http://osm.org">OpenStreetMap</a> contributors, &copy; <a href="https://carto.com/">CARTO</a>',      }),    });

2.渲染3d建筑物轮廓

这一步最重要的是要有带高度的建筑物数据,百度,高德,天地图等专业地图商均不提供数据。因此数据获取方式 1.网友分享 2.测绘机构发布 3.第三方购买。以上三种方式都不能确保数据真实性,因此不适用精准度要求高的需求。

一般找的的数据是shp格式的,可以通过在线转换得到geojson格式的数据,就可以在页面上使用了。转换地址:mapshaper.org/

数据分享
1.网友分享:国内77个城市建筑物轮廓数据,这个数据的坐标是utm,需要通过arcmap进行转换得到WGS84坐标。cad格式的数据:mp.weixin.qq.com/s/GMyGv4IRH…

shp数据:www.bilibili.com/read/cv1704…

感谢两位作者。

2.测绘机构

90座城市建筑物数据:data.tpdc.ac.cn/zh-hans/dat…

3.购买

淘宝可以按选择区域购买,每次可选区域比较小。

代码实现:参考maptalks-three提供的demo,github.com/maptalks/ma…

这一块的代码主要是解析geojson,生成3d建筑物添加到threelayer中,并添加图片材质。主要是three.js的相关内容。

贴图图片,可以任意更改图片以及建筑物顶部颜色得到不同展示效果的建筑物轮廓。刚开始没想到最酷的效果是用最简单的图片实现的。

3.根据经纬度添加信息卡片

这里为了能使用好看的卡片样式,采用maptalks的ui覆盖物,可以添加dom元素,随意定制css样式。

这里发现了一个小技巧,根据单位名称,通过天眼查的付费接口获取到单位的详细信息,再根据高德地图通过地址查询经纬度坐标,再将高德地图的GCJ02坐标转为WGS84坐标,就能够在3d地图上找到要定位的点。

完整代码参考:

<template>  <div id="map"></div></template><script>import axios from "axios";import * as THREE from "three";import * as maptalks from "maptalks";import { ThreeLayer, BaseObject } from "maptalks.three";import gcoord from "gcoord";import buildings from "assets/js/cangzhou.js";import buildImg from "assets/imgs/build2.png";import buildIcon from "assets/imgs/1.svg";//真实使用时通过天眼查接口获取import addressList from "assets/js/addressList.js";buildings[0].features = buildings[0].features.filter((item) => item.geometry);export default {  name: "",  data() {    return {      map: null,      threeLayer: null,      heightPerLevel: 1.2,    };  },  mounted() {    this.initMap();    this.initThreeLayer();    setTimeout(() => {      // this.setAddressInfo();    }, 2000);  },  methods: {    initMap() {      this.map = new maptalks.Map("map", {        center: [116.83091872287843, 38.300708019500924], //地图定位点        zoom: 17, //缩放        maxZoom: 16.5,        minZoom: 15.5,        pitch: 70, //倾斜度        bearing: 0,        //加载地图地图,可选百度        baseLayer: new maptalks.TileLayer("tile", {          urlTemplate:            "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",          subdomains: ["a", "b", "c", "d"],          attribution:            '&copy; <a href="http://osm.org">OpenStreetMap</a> contributors, &copy; <a href="https://carto.com/">CARTO</a>',        }),      });    },    initThreeLayer() {      this.threeLayer = new ThreeLayer("t", {        forceRenderOnMoving: false,        forceRenderOnRotating: false,        animation: false,      });      this.threeLayerPrepareDraw();      this.threeLayer.addTo(this.map);      this.animation();    },    //设置three 灯光,相机等    threeLayerPrepareDraw() {      this.threeLayer.prepareToDraw = (gl, scene, camera) => {        var light = new THREE.DirectionalLight(0xffffff); //光 方向光        light.position.set(0, 10, -0).normalize(); //光的位置        scene.add(light);        camera.add(new THREE.PointLight("#fff", 2));        camera.near = 0.1;        camera.far = 10000;        // camera        //    const camera = this.threeLayer.getCamera();        this.addBuildings();      };    },    addBuildings() {      var meshs = [];      var features = [];      buildings.forEach(function (b) {        features = features.concat(b.features);      });      // 设置几何体数组      var polygons = features.map((f, index) => {        const polygon = maptalks.GeoJSON.toGeometry(f);        var levels =          f.properties.levels ||          f.properties.Floor ||          f.properties.Elevation ||          1;        polygon.setProperties({          height: this.heightPerLevel * levels,        });        return polygon;      });      var material = this.getBuildingsMaterial(); //设置建筑材质,贴图      material.vertexColors = THREE.VertexColors;      //根据json数据加载几何体并加载贴图材质,创建网格      const mesh = this.threeLayer.toExtrudePolygons(        polygons,        { interactive: false },        material      );      this.setTopColor(mesh);      meshs.push(mesh);      this.threeLayer.addMesh(meshs);    },    setTopColor(mesh) {      const topColor = new THREE.Color("#3d5bb6"); //建筑物顶点颜色      const bufferGeometry = mesh.getObject3d().geometry; //缓冲区几何体      const position = bufferGeometry.attributes.position.array;      const uv = bufferGeometry.attributes.uv.array;      const color = bufferGeometry.attributes.color.array;      const index = mesh.getObject3d().geometry.index.array;      for (let i = 0, len = index.length; i < len; i += 3) {        const a = index[i],          b = index[i + 1],          c = index[i + 2];        const z1 = position[a * 3 + 2],          z2 = position[b * 3 + 2],          z3 = position[c * 3 + 2];        // top vertex        if (z1 > 0 && z2 > 0 && z3 > 0) {          uv[a * 2] = 0;          uv[a * 2 + 1] = 0;          uv[b * 2] = 0;          uv[b * 2 + 1] = 0;          uv[c * 2] = 0;          uv[c * 2 + 1] = 0;          color[a * 3] = topColor.r;          color[a * 3 + 1] = topColor.g;          color[a * 3 + 2] = topColor.b;          color[b * 3] = topColor.r;          color[b * 3 + 1] = topColor.g;          color[b * 3 + 2] = topColor.b;          color[c * 3] = topColor.r;          color[c * 3 + 1] = topColor.g;          color[c * 3 + 2] = topColor.b;        }      }    },    // 贴图纹理    getBuildingsMaterial(color = "red") {      const texture = new THREE.TextureLoader().load(buildImg);      texture.needsUpdate = true; //使用贴图时进行更新      texture.wrapS = texture.wrapT = THREE.RepeatWrapping;      texture.repeat.set(1, 1);      const material = new THREE.MeshPhongMaterial({        map: texture,        transparent: false,        color: "#fff",        depthWrite: true,      });      return material;    },    animation() {      // layer animation support Skipping frames      this.threeLayer._needsUpdate = !this.threeLayer._needsUpdate;      if (this.threeLayer._needsUpdate) {        this.threeLayer.redraw();      }      requestAnimationFrame(this.animation);    },    //设置卡片显示    setAddressInfo() {      let num = 1;      for (let item of addressList) {        if (item.regLocation) {          //获取经纬度坐标          axios            .get(              `https://restapi.amap.com/v3/geocode/geo?address=${item.regLocation}&key=4d83c16688d6c2d14896036476d6ef54`            )            .then((res) => {              num += 3;              if (res.data.geocodes && res.data.geocodes[0]) {                let location = res.data.geocodes[0].location;                let transformLocation = gcoord.transform(                  location.split(","), // 经纬度坐标                  gcoord.GCJ02, // 目标坐标系                  gcoord.WGS84 // 当前坐标系                );                item.lonLat = transformLocation;                setTimeout(() => {                  this.addInfoCard(item);                }, num * 1000);              }            });        }      }    },    addInfoCard(info) {      this.map.animateTo(        {          center: info.lonLat,          zoom: 17,        },        {          duration: 1000,          easing: "out",        }      );      setTimeout(() => {        var dom = document.createElement("div");        dom.classList.add("map-info-box");        dom.innerHTML = `<h3>${info.name}</h3>      <div><span class="label">漏洞类型:</span>会话管理</div>      <div><span class="label">漏洞级别:</span><span style="color:red;">紧急</span></div>`;        var marker = new maptalks.ui.UIMarker(info.lonLat, {          content: dom,          // minZoom: 0,          // maxZoom: 18,          animation: "scale",          altitude: 0,          animationDuration: 1000,        }).addTo(this.map);      }, 800);      // marker.addEventListener("click", (e) => {      //   this.map.animateTo(      //     {      //       center: e.coordinate,      //       zoom: 17,      //     },      //     {      //       duration: 1000,      //       easing: "out",      //     }      //   );      // });    },  },};</script><style lang="scss">@keyframes fadeIn {  from {    opacity: 0;  }  to {    opacity: 1;  }}.map-info-box {  width: 240px;  padding: 5px;  background: #162a38;  color: #fff;  border-radius: 2px;  box-shadow: 0 0 2px 2px #ccc;  font-size: 12px;  cursor: pointer;  animation-name: fadeIn;  animation-duration: 1s;  h3 {    margin: 0;    font-size: 12px;    display: flex;  }  address {    font-style: normal;    display: flex;  }  .label {    color: #aaa;    font-weight: normal;    display: inline-block;    width: 5em;    flex-shrink: 0;  }}</style>