百度地图使用的是百度地图自定义的BD09坐标系,是在火星坐标系的基础上二次加密得到。因此cesium在加载百度地图服务底图时,也存在偏差和纠偏的问题。是否纠偏,根据具体的业务需求。本文通过引用第三方纠偏库,同时支持cesium加载百度BD09坐标系及纠偏后的WGS84坐标系。
1. 引入第三方纠偏库
2. 自定义百度纠偏切片方案
百度地图的切片方式和web墨卡托的切片方式不同,需要将百度切片方式转换为web墨卡托的切片方式,具体实现原理较为复杂,这里不去解释,后续有时间,在webgis的底层原理里进行分析。
2.1 自定义百度地图墨卡托投影类
BaiduMercatorProjection.js
const MC_BAND = [12890594.86, 8362377.87, 5591021, 3481989.83, 1678043.12, 0]
const LL_BAND = [75, 60, 45, 30, 15, 0]
const MC2LL = [
[
1.410526172116255e-8,
8.98305509648872e-6,
-1.9939833816331,
2.009824383106796e2,
-1.872403703815547e2,
91.6087516669843,
-23.38765649603339,
2.57121317296198,
-0.03801003308653,
1.73379812e7
],
[
-7.435856389565537e-9,
8.983055097726239e-6,
-0.78625201886289,
96.32687599759846,
-1.85204757529826,
-59.36935905485877,
47.40033549296737,
-16.50741931063887,
2.28786674699375,
1.026014486e7
],
[
-3.030883460898826e-8,
8.98305509983578e-6,
0.30071316287616,
59.74293618442277,
7.357984074871,
-25.38371002664745,
13.45380521110908,
-3.29883767235584,
0.32710905363475,
6.85681737e6
],
[
-1.981981304930552e-8,
8.983055099779535e-6,
0.03278182852591,
40.31678527705744,
0.65659298677277,
-4.44255534477492,
0.85341911805263,
0.12923347998204,
-0.04625736007561,
4.48277706e6
],
[
3.09191371068437e-9,
8.983055096812155e-6,
0.00006995724062,
23.10934304144901,
-0.00023663490511,
-0.6321817810242,
-0.00663494467273,
0.03430082397953,
-0.00466043876332,
2.5551644e6
],
[
2.890871144776878e-9,
8.983055095805407e-6,
-0.00000003068298,
7.47137025468032,
-0.00000353937994,
-0.02145144861037,
-0.00001234426596,
0.00010322952773,
-0.00000323890364,
8.260885e5
]
]
const LL2MC = [
[
-0.0015702102444,
1.113207020616939e5,
1.704480524535203e15,
-1.033898737604234e16,
2.611266785660388e16,
-3.51496691766537e16,
2.659570071840392e16,
-1.072501245418824e16,
1.800819912950474e15,
82.5
],
[
8.277824516172526e-4,
1.113207020463578e5,
6.477955746671608e8,
-4.082003173641316e9,
1.077490566351142e10,
-1.517187553151559e10,
1.205306533862167e10,
-5.124939663577472e9,
9.133119359512032e8,
67.5
],
[
0.00337398766765,
1.113207020202162e5,
4.481351045890365e6,
-2.339375119931662e7,
7.968221547186455e7,
-1.159649932797253e8,
9.723671115602145e7,
-4.366194633752821e7,
8.477230501135234e6,
52.5
],
[
0.00220636496208,
1.113207020209128e5,
5.175186112841131e4,
3.796837749470245e6,
9.920137397791013e5,
-1.22195221711287e6,
1.340652697009075e6,
-6.209436990984312e5,
1.444169293806241e5,
37.5
],
[
-3.441963504368392e-4,
1.113207020576856e5,
2.782353980772752e2,
2.485758690035394e6,
6.070750963243378e3,
5.482118345352118e4,
9.540606633304236e3,
-2.71055326746645e3,
1.405483844121726e3,
22.5
],
[
-3.218135878613132e-4,
1.113207020701615e5,
0.00369383431289,
8.237256402795718e5,
0.46104986909093,
2.351343141331292e3,
1.58060784298199,
8.77738589078284,
0.37238884252424,
7.45
]
]
class BaiduMercatorProjection {
constructor() {
this.isWgs84 = false
}
convertMC2LL(point) {
if (!point) {
return { lng: 0, lat: 0 }
}
let lnglat = {}
if (this.isWgs84) {
lnglat.lng = (point.lng / 20037508.34) * 180
let mmy = (point.lat / 20037508.34) * 180
lnglat.lat =
(180 / Math.PI) *
(2 * Math.atan(Math.exp((mmy * Math.PI) / 180)) - Math.PI / 2)
return {
lng: lnglat['lng'].toFixed(6),
lat: lnglat['lat'].toFixed(6)
}
}
let temp = {
lng: Math.abs(point['lng']),
lat: Math.abs(point['lat'])
}
let factor = undefined
for (let i = 0; i < MC_BAND.length; i++) {
if (temp['lat'] >= MC_BAND[i]) {
factor = MC2LL[i]
break
}
}
lnglat = this.convertor(point, factor)
return {
lng: lnglat['lng'].toFixed(6),
lat: lnglat['lat'].toFixed(6)
}
}
convertLL2MC(point) {
if (!point) {
return { lng: 0, lat: 0 }
}
if (
point['lng'] > 180 ||
point['lng'] < -180 ||
point['lat'] > 90 ||
point['lat'] < -90
) {
return point
}
if (this.isWgs84) {
let mercator = {}
let earthRad = 6378137.0
mercator.lng = ((point.lng * Math.PI) / 180) * earthRad
let a = (point.lat * Math.PI) / 180
mercator.lat =
(earthRad / 2) * Math.log((1.0 + Math.sin(a)) / (1.0 - Math.sin(a)))
return {
lng: parseFloat(mercator['lng'].toFixed(2)),
lat: parseFloat(mercator['lat'].toFixed(2))
}
}
point['lng'] = this.getLoop(point['lng'], -180, 180)
point['lat'] = this.getRange(point['lat'], -74, 74)
let temp = { lng: point['lng'], lat: point['lat'] }
let factor = undefined
for (let i = 0; i < LL_BAND.length; i++) {
if (temp['lat'] >= LL_BAND[i]) {
factor = LL2MC[i]
break
}
}
if (!factor) {
for (let i = 0; i < LL_BAND.length; i++) {
if (temp['lat'] <= -LL_BAND[i]) {
factor = LL2MC[i]
break
}
}
}
let mc = this.convertor(point, factor)
return {
lng: parseFloat(mc['lng'].toFixed(2)),
lat: parseFloat(mc['lat'].toFixed(2))
}
}
convertor(fromPoint, factor) {
if (!fromPoint || !factor) {
return { lng: 0, lat: 0 }
}
let x = factor[0] + factor[1] * Math.abs(fromPoint['lng'])
let temp = Math.abs(fromPoint['lat']) / factor[9]
let y =
factor[2] +
factor[3] * temp +
factor[4] * temp * temp +
factor[5] * temp * temp * temp +
factor[6] * temp * temp * temp * temp +
factor[7] * temp * temp * temp * temp * temp +
factor[8] * temp * temp * temp * temp * temp * temp
x *= fromPoint['lng'] < 0 ? -1 : 1
y *= fromPoint['lat'] < 0 ? -1 : 1
return {
lng: x,
lat: y
}
}
getRange(v, a, b) {
if (a != null) {
v = Math.max(v, a)
}
if (b != null) {
v = Math.min(v, b)
}
return v
}
getLoop(v, a, b) {
while (v > b) {
v -= b - a
}
while (v < a) {
v += b - a
}
return v
}
lngLatToMercator(point) {
return this.convertLL2MC(point)
}
mercatorToLngLat(point) {
return this.convertMC2LL(point)
}
}
export default BaiduMercatorProjection
2.2 自定义百度地图切片方案
BaiduMercatorTilingScheme.js
import BaiduMercatorProjection from './BaiduMercatorProjection'
import gcoord from 'gcoord';
class BaiduMercatorTilingScheme extends Cesium.WebMercatorTilingScheme {
constructor(options) {
super(options)
let projection = new BaiduMercatorProjection()
this._projection.project = function(cartographic, result) {
result = result || {}
// wgs84坐标系转BD09坐标系
result = gcoord.transform([
Cesium.Math.toDegrees(cartographic.longitude),
Cesium.Math.toDegrees(cartographic.latitude)
], gcoord.WGS84, gcoord.BD09)
result[0] = Math.min(result[0], 180)
result[0] = Math.max(result[0], -180)
result[1] = Math.min(result[1], 74.000022)
result[1] = Math.max(result[1], -71.988531)
result = projection.lngLatToMercator({
lng: result[0],
lat: result[1]
})
return new Cesium.Cartesian2(result.lng, result.lat)
}
this._projection.unproject = function(cartesian, result) {
result = result || {}
result = projection.mercatorToLngLat({
lng: cartesian.x,
lat: cartesian.y
})
// BD09坐标系转wgs84坐标系
result = gcoord.transform([
result.lng,
result.lat
], gcoord.BD09, gcoord.WGS84)
return new Cesium.Cartographic(
Cesium.Math.toRadians(result[0]),
Cesium.Math.toRadians(result[1])
)
}
this.resolutions = options.resolutions || []
}
tileXYToNativeRectangle(x, y, level, result) {
const tileWidth = this.resolutions[level]
const west = x * tileWidth
const east = (x + 1) * tileWidth
const north = ((y = -y) + 1) * tileWidth
const south = y * tileWidth
if (!Cesium.defined(result)) {
return new Cesium.Rectangle(west, south, east, north)
}
result.west = west
result.south = south
result.east = east
result.north = north
return result
}
positionToTileXY(position, level, result) {
const rectangle = this._rectangle
if (!Cesium.Rectangle.contains(rectangle, position)) {
return undefined
}
const projection = this._projection
const webMercatorPosition = projection.project(position)
if (!Cesium.defined(webMercatorPosition)) {
return undefined
}
const tileWidth = this.resolutions[level]
const xTileCoordinate = Math.floor(webMercatorPosition.x / tileWidth)
const yTileCoordinate = -Math.floor(webMercatorPosition.y / tileWidth)
if (!Cesium.defined(result)) {
return new Cesium.Cartesian2(xTileCoordinate, yTileCoordinate)
}
result.x = xTileCoordinate
result.y = yTileCoordinate
return result
}
}
export default BaiduMercatorTilingScheme
2.3 自定义百度地图提供器
BaiduImageryProvider.js
import BaiduMercatorTilingScheme from './BaiduMercatorTilingScheme'
// 影像底图
const IMG_URL = 'http:maponline{s}.bdimg.com/starpic/u=x={x};y={y};z={z};v=009;type=sate&qt=satepc&fm=46&app=webearth2&v=009'
// 影像标注
const IMG_LABEL_URL = 'http:maponline{s}.bdimg.com/tile/?x={x}&y={y}&z={z}&qt=vtile&styles=sl&showtext=1&scaler=2&v=083'
// 电子底图无标注
const VEC_URL ='http:maponline{s}.bdimg.com/tile/?x={x}&y={y}&z={z}&qt=vtile&styles=pl&showtext=0&scaler=1&v=083'
// 电子地图有标注
const VEC_LABEL_URL ='http:maponline{s}.bdimg.com/tile/?x={x}&y={y}&z={z}&qt=vtile&styles=pl&showtext=1&scaler=2&v=083'
class BaiduImageryProvider {
constructor(options = {}) {
switch(options.type) {
case 'img_w':
this._url = IMG_URL
break
case 'img_label_w':
this._url = IMG_LABEL_URL
break
case 'vec_w':
this._url = VEC_URL
break
case 'vec_label_w':
this._url = VEC_LABEL_URL
break
}
this._tileWidth = 256
this._tileHeight = 256
this._maximumLevel = 18
this._crs = options.crs || 'BD09'
if (options.crs === 'WGS84') {
let resolutions = []
for (let i = 0; i < 19; i++) {
resolutions[i] = 256 * Math.pow(2, 18 - i)
}
this._tilingScheme = new BaiduMercatorTilingScheme({
resolutions,
rectangleSouthwestInMeters: new Cesium.Cartesian2(
-20037726.37,
-12474104.17
),
rectangleNortheastInMeters: new Cesium.Cartesian2(
20037726.37,
12474104.17
)
})
} else {
this._tilingScheme = new Cesium.WebMercatorTilingScheme({
rectangleSouthwestInMeters: new Cesium.Cartesian2(-33554054, -33746824),
rectangleNortheastInMeters: new Cesium.Cartesian2(33554054, 33746824)
})
}
this._rectangle = this._tilingScheme.rectangle
this._credit = undefined
this._style = options.style || 'normal'
}
get url() {
return this._url
}
get token() {
return this._token
}
get tileWidth() {
if (!this.ready) {
throw new Cesium.DeveloperError(
'tileWidth must not be called before the imagery provider is ready.'
)
}
return this._tileWidth
}
get tileHeight() {
if (!this.ready) {
throw new Cesium.DeveloperError(
'tileHeight must not be called before the imagery provider is ready.'
)
}
return this._tileHeight
}
get maximumLevel() {
if (!this.ready) {
throw new Cesium.DeveloperError(
'maximumLevel must not be called before the imagery provider is ready.'
)
}
return this._maximumLevel
}
get minimumLevel() {
if (!this.ready) {
throw new Cesium.DeveloperError(
'minimumLevel must not be called before the imagery provider is ready.'
)
}
return 0
}
get tilingScheme() {
if (!this.ready) {
throw new Cesium.DeveloperError(
'tilingScheme must not be called before the imagery provider is ready.'
)
}
return this._tilingScheme
}
get rectangle() {
if (!this.ready) {
throw new Cesium.DeveloperError(
'rectangle must not be called before the imagery provider is ready.'
)
}
return this._rectangle
}
get ready() {
return !!this._url
}
get credit() {
return this._credit
}
get hasAlphaChannel() {
return true
}
getTileCredits(x, y, level) {}
requestImage(x, y, level) {
if (!this.ready) {
throw new Cesium.DeveloperError(
'requestImage must not be called before the imagery provider is ready.'
)
}
let xTiles = this._tilingScheme.getNumberOfXTilesAtLevel(level)
let yTiles = this._tilingScheme.getNumberOfYTilesAtLevel(level)
let url = this._url
.replace('{z}', level)
.replace('{s}', String(1))
if (this._crs === 'WGS84') {
url = url.replace('{x}', String(x)).replace('{y}', String(-y))
} else {
url = url
.replace('{x}', String(x - xTiles / 2))
.replace('{y}', String(yTiles / 2 - y - 1))
}
return Cesium.ImageryProvider.loadImage(this, url)
}
}
export default BaiduImageryProvider
3. 完整示例代码
MapWorks.js
/*
* @Description:
* @Author: maizi
* @Date: 2023-04-03 17:34:21
* @LastEditTime: 2023-04-04 13:55:20
* @LastEditors: maizi
*/
import BaiduImageryProvider from './BaiduImageryProvider'
// 初始视图定位在中国
Cesium.Camera.DEFAULT_VIEW_RECTANGLE = Cesium.Rectangle.fromDegrees(90, -20, 110, 90);
let viewer = null;
function initMap(container) {
viewer = new Cesium.Viewer(container, {
animation: false,
baseLayerPicker: false,
fullscreenButton: false,
geocoder: false,
homeButton: false,
infoBox: false,
sceneModePicker: false,
selectionIndicator: false,
timeline: false,
navigationHelpButton: false,
scene3DOnly: true,
orderIndependentTranslucency: false,
contextOptions: {
webgl: {
alpha: true
}
}
})
viewer._cesiumWidget._creditContainer.style.display = 'none'
viewer.scene.fxaa = true
viewer.scene.postProcessStages.fxaa.enabled = true
if (Cesium.FeatureDetection.supportsImageRenderingPixelated()) {
// 判断是否支持图像渲染像素化处理
viewer.resolutionScale = window.devicePixelRatio
}
// 移除默认影像
removeAll()
// 地形深度测试
viewer.scene.globe.depthTestAgainstTerrain = true
// 背景色
viewer.scene.globe.baseColor = new Cesium.Color(0.0, 0.0, 0.0, 0)
}
function addBdLayer(options) {
const layerProvider = new BaiduImageryProvider(options);
viewer.imageryLayers.addImageryProvider(layerProvider);
}
function changeBaseMap(type,enabled) {
removeAll()
switch(type) {
case 0: //影像地图
addBdLayer({
type: 'img_w',
crs: enabled ? 'WGS84' : null
})
addBdLayer({
type: 'img_label_w',
crs: enabled ? 'WGS84' : null
})
break
case 1: //电子地图无标注
addBdLayer({
type: 'vec_w',
crs: enabled ? 'WGS84' : null
})
break
case 2: //电子地图有标注
addBdLayer({
type: 'vec_label_w',
crs: enabled ? 'WGS84' : null
})
break
}
}
function removeAll() {
viewer.imageryLayers.removeAll();
}
function destroy() {
viewer.entities.removeAll();
viewer.imageryLayers.removeAll();
viewer.destroy();
}
function setView(coords,hpr) {
viewer.scene.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(coords[0], coords[1], coords[2]),
orientation: {
heading: Cesium.Math.toRadians(hpr.heading),
pitch: Cesium.Math.toRadians(hpr.pitch),
roll: Cesium.Math.toRadians(hpr.roll),
}
});
}
export {
initMap,
addBdLayer,
changeBaseMap,
setView,
removeAll,
destroy
}
BdLayer.vue
<!--
* @Description:
* @Author: maizi
* @Date: 2023-04-03 11:34:28
* @LastEditTime: 2023-04-04 15:41:15
* @LastEditors: maizi
-->
<template>
<div id="container">
<div class="pane_container">
<el-checkbox v-model="checked" class="my-check">纠偏</el-checkbox>
<el-button size="small" @click="changeMap(0)">影像地图</el-button>
<el-button size="small" @click="changeMap(1)">电子地图(无标注)</el-button>
<el-button size="small" @click="changeMap(2)">电子地图(有标注)</el-button>
</div>
</div>
</template>
<script>
import * as MapWorks from './js/MapWorks'
export default {
name: 'GdLayer',
data(){
return {
checked: false
}
},
mounted() {
this.init();
},
beforeDestroy(){
//实例被销毁前调用,页面关闭、路由跳转、v-if和改变key值
MapWorks.destroy();
},
methods:{
init(){
let container = document.getElementById("container");
MapWorks.initMap(container)
MapWorks.changeBaseMap(0)
let coords = [104.0634012222290039, 30.6598806381225586, 500]
let hpr = {
heading: 0,
pitch:-90.0,
roll: 0
}
MapWorks.setView(coords, hpr)
},
changeMap(index){
MapWorks.changeBaseMap(index, this.checked)
}
}
}
</script>
<style lang="scss" scoped>
#container{
width: 100%;
height: 100%;
background: rgba(7, 12, 19, 1);
overflow: hidden;
background-size: 40px 40px, 40px 40px;
background-image: linear-gradient(hsla(0, 0%, 100%, 0.05) 1px, transparent 0), linear-gradient(90deg, hsla(0, 0%, 100%, 0.05) 1px, transparent 0);
.pane_container{
margin-top: 12px;
margin-left: 12px;
position: absolute;
padding: 10px 15px;
border-radius: 4px;
border: 1px solid rgba(128, 128, 128, 0.5);
color: #ffffff;
background: rgba(0, 0, 0, 0.4);
box-shadow: 0 3px 14px rgb(128 128 128 / 50%);
z-index: 2;
.my-check{
margin-right: 12px;
}
}
}
</style>