前言
前端时间详细讲了一下高程点到DEM,再到热力图的一连串自动化处理步骤,热力图是一种可视化高程的方法,如果想要立体三维的效果,请细看以下内容。本文我将用两种方法来三维可视化DEM,将会用到cesium和maptalks两种地图引擎。
处理流程
(一)cesium
Tif转Terrain
直接用cesiumlab转,参数如图,直接提交生成地形文件。
开源代码处理
参考github上一个地形处理工具,生成地形。 https://github.com/geo-data/cesium-terrain-builder
cesium地形加载
用cesium以下代码加载一下。
var terrainProvider = new Cesium.CesiumTerrainProvider({
url:"/Terrian/2",
minimumLevel: 0,
maximumLevel: 20,
})
window.viewer.terrainProvider = terrainProvider;
window.viewer.scene.globe.terrainExaggeration = 20.0;//地形夸张
效果
红线是我描绘的以防看不清。
(二)Maptalks
maptalks是一个国内开发的地图引擎,其对三维模型、3dtiles等的拓展使其兼顾着二三维的渲染效果,如果了解threejs还可以根据其插件做threejs相关开发,是一款挺不错的地图引擎。最近也是有用到做一个项目,开发起来很便捷。那maptalks是如何渲染地形,我们只需要参考其例子即可。 https://maptalks.org/maptalks.three/demo/terrain.html
代码分析
主要看这个函数,其用到两个服务 服务1:https://api.mapbox.com/v4/mapbox.satellite/${z}/${x}/${y}.png?access_token=${accesstoken} mapbox影像服务,用于对地形做纹理 服务2:https://a.tiles.mapbox.com/v4/mapbox.terrain-rgb/${z}/${x}/${y}.pngraw?access_token=${accesstoken} mapbox地形RGB服务,用于生成地形高度
function addTerrain() {
const TILESIZE = 256;
const minx = 3268, maxx = 3277, miny = 1632, maxy = 1641;
const tiles = [];
for (let x = minx; x <= maxx; x++) {
for (let y = miny; y <= maxy; y++) {
tiles.push([x, y, 12]);
}
}
tiles.forEach(tile => {
const [x, y, z] = tile;
const bbox = tilebelt.tileToBBOX(tile);
const texture = `https://api.mapbox.com/v4/mapbox.satellite/${z}/${x}/${y}.png?access_token=${accesstoken}`;
const terrain = threeLayer.toTerrain(bbox, { texture, imageWidth: TILESIZE, imageHeight: TILESIZE }, new THREE.MeshBasicMaterial());
// const sclae = 1.005;
// terrain.getObject3d().scale.set(sclae, sclae, 1);
threeLayer.addMesh(terrain);
tileData.push({
tile,
terrain
});
});
setTimeout(() => {
updateTerrainData();
}, 2000);
}
function updateTerrainData() {
tileData.forEach(d => {
const { tile, terrain } = d;
const [x, y, z] = tile;
const url = `https://a.tiles.mapbox.com/v4/mapbox.terrain-rgb/${z}/${x}/${y}.pngraw?access_token=${accesstoken}`;
actor.test({ tile, url }, (message) => {
if (message.data) {
taskQueue.push({
data: new Uint32Array(message.data),
terrain
})
}
})
});
}
需要关注几个值如下,这几个值要和你生成的瓦片数据一一对应,或许你只能获得某一个地区的DEM数据,这个地区的瓦片对应在哪里,找到你想要的层级和对应范围。(说得有点抽象,不懂可以研究下瓦片地图金字塔模型)
制作Mapbox Terrain-RGB(mapbox地形RGB服务)
经过代码分析发现我们主要需要的就是一个类似MapboxTerrain-RGB的服务,以下就是这个服务的做法:
1、安装处理程序
安装前必须还需要安装python环境 gdal 栅格和矢量地理空间数据处理 rasterio 读写栅格数据; rio-rgbify 将 dem 栅格编码为 rgb 栅格的 rasterio 插件;
2、tif数据处理
在数据存储目录cmd
第一步旨在将坐标系转化为3857,同时将无数据的值化为null。
gdalwarp -t_srs EPSG:3857 -dstnodata None -co TILED=YES -co COMPRESS=DEFLATE
-co BIGTIFF=IF_NEEDED 输入.tif 输出.tif
第二步旨在制作出符合Mapbox Terrain-RGB模型的tif数据 具体模型可以参考Mapbox Terrain-RGB
rio rgbify -b -10000 -i 0.1 输出.tif 第二次输出.tif
3、服务发布
geoserver发布栅格tif即可
完整代码
<template>
<div id="map"></div>
</template>
<script>
import * as maptalks from 'maptalks';
import * as THREE from 'three';
import { GroupGLLayer } from "@maptalks/gl-layers"
import { ThreeLayer } from 'maptalks.three'
import tilebelt from '../lib/map/layer/tilebelt'
let accesstoken = '你的token';
export default {
name: "TerrainView",
mounted() {
const workerKey = 'terraindata'
maptalks.registerWorkerAdapter(workerKey, function (exports, global) {
let canvas;
const TILESIZE = 256;
function getCanvas() {
if (canvas) {
return canvas;
}
canvas = new OffscreenCanvas(TILESIZE, TILESIZE);
return canvas;
}
//will be called only for once when loaded in worker thread
exports.initialize = function () {
// console.log('terraindata worker initialized');
};
//to receive message from main thread sent by maptalks.worker.Actor
exports.onmessage = function (message, postResponse) {
const data = message.data;
const { url } = data;
fetch(url).then(res => res.blob()).then(blob => createImageBitmap(blob)).then(bitmap => {
const canvas = getCanvas();
const offCtx = canvas.getContext('2d');
offCtx.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
var imgData = offCtx.getImageData(0, 0, canvas.width, canvas.height).data;
bitmap.close();
const data = new Uint32Array(imgData).buffer;
postResponse(null, { data: data }, [data]);
}).catch(error => {
console.log(error)
postResponse(null, {}, []);
});
};
})
const actor = new maptalks.worker.Actor(workerKey);
actor.test = function (params, callback) {
const { url, tile } = params;
this.send({ url }, null, (err, message) => {
message.tile = tile;
callback(message);
});
}
var baseLayer = new maptalks.TileLayer('tile', {
// urlTemplate: 'https://mt2.google.cn/maps/vt?lyrs=s&hl=zh-CN&gl=CN&x={x}&y={y}&z={z}',
urlTemplate: 'https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}.png?access_token=' + accesstoken,
subdomains: ['a', 'b', 'c', 'd'],
debug: true,
debugOutline: 'red'
// attribution: '© <a href="http://osm.org">OpenStreetMap</a> contributors, © <a href="https://carto.com/">CARTO</a>'
});
var map = new maptalks.Map("map", {
"center": [109.16032190999756, 19.81946033063278], "zoom": 16, "pitch": 58.40000000000002, "bearing": -0.6000000000000227,
// bearing: 180,
centerCross: false,
doubleClickZoom: false,
baseLayer: baseLayer,
zoomControl: true,
heightFactor: 1.4
});
// baseLayer.hide();
// the ThreeLayer to draw buildings
var threeLayer = new ThreeLayer('t', {
forceRenderOnMoving: true,
forceRenderOnRotating: true
// animation: true
});
threeLayer.prepareToDraw = function (gl, scene, camera) {
var light = new THREE.DirectionalLight(0xffffff);
light.position.set(0, -10, 10).normalize();
scene.add(light);
addTerrain();
animation();
}
const tileData = [];
function addTerrain() {
const TILESIZE = 256;
const minx = 52637, maxx = 52641, miny = 29085, maxy = 29089;
const tiles = [];
for (let x = minx; x <= maxx; x++) {
for (let y = miny; y <= maxy; y++) {
tiles.push([x, y, 16]);
}
}
tiles.forEach(tile => {
const [x, y, z] = tile;
const bbox = tilebelt.tileToBBOX(tile);
const texture = `https://api.mapbox.com/v4/mapbox.satellite/${z}/${x}/${y}.png?access_token=${accesstoken}`;
//let texture = `/geoserverAddress/geoserver/cite/gwc/service/wmts?layer=cite:3857_gd_dem_n_rgb&style=&tilematrixset=EPSG:900913&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image/png&TileMatrix=EPSG:900913:${z}&TileCol=${x}&TileRow=${y}`
const terrain = threeLayer.toTerrain(bbox, { texture, imageWidth: TILESIZE, imageHeight: TILESIZE }, new THREE.MeshBasicMaterial());
// const sclae = 1.005;
// terrain.getObject3d().scale.set(sclae, sclae, 1);
threeLayer.addMesh(terrain);
tileData.push({
tile,
terrain
});
});
setTimeout(() => {
updateTerrainData();
}, 10);
}
const taskQueue = [];
function updateTerrainData() {
tileData.forEach(d => {
const { tile, terrain } = d;
const [x, y, z] = tile;
const url = `http://localhost:8888/geoserverAddress/geoserver/cite/gwc/service/wmts?layer=cite%3A3857_gd_dem_n_rgb&style=&tilematrixset=EPSG%3A900913&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image%2Fpng&TileMatrix=EPSG%3A900913%3A${z}&TileCol=${x}&TileRow=${y}`
actor.test({ tile, url }, (message) => {
console.log(message.data)
if (message.data) {
taskQueue.push({
data: new Uint32Array(message.data),
terrain
})
}
})
});
}
function loopQueue() {
if (taskQueue.length) {
const { data, terrain } = taskQueue[0];
terrain.updateData(data);
taskQueue.splice(0, 1);
}
}
const sceneConfig = {
postProcess: {
enable: true,
antialias: { enable: true }
}
};
const groupLayer = new GroupGLLayer('group', [threeLayer], { sceneConfig });
groupLayer.addTo(map);
function animation() {
// layer animation support Skipping frames
threeLayer._needsUpdate = !threeLayer._needsUpdate;
if (threeLayer._needsUpdate && !threeLayer.isRendering()) {
threeLayer.redraw();
loopQueue();
}
requestAnimationFrame(animation);
}
function getTerrains() {
return tileData.map(d => {
return d.terrain;
})
}
}
}
</script>
<style scoped>
#map{
width: 100%;
height: 100%;
}
</style>
效果
结尾
对我来说,以前想到地形三维展示就想到cesium,但像maptalks这样的地图引擎同样也可以做到类似的事情,这对于技术选型来说又多了一种选择。