Cesium对坐标系支持情况
Cesium可以加载WMTS、WMS、ArcGIS已经谷歌、必应、天地图等在线服务,但是这些服务都是Web墨卡托或者WGS坐标系,并且Cesium也只支持WGS84(EPSG:43226)、Web墨卡托(EPSG:3857)坐标系的WMTS、ArcGIS MapServer服务,当你加载其他服务的时候,Cesium会提示An error occurred in "WebMapTileServiceImageryProvider": Failed to obtain image tile X: 0 Y: 0 Level: 1.
或者An error occurred in "St": Tile spatial reference WKID 4526 is not supported.
原理
WMTS服务
Cesium提供了统一的接口ImageryProvider
来加载多种地图服务,对于WMTS服务,Cesium提供了WebMapTileServiceImageryProvider
,支持WGS84(EPSG:43226)、Web墨卡托(EPSG:3857)坐标系。同时,WebMapTileServiceImageryProvider
构造函数中还提供了tilingScheme: TilingScheme
字段,允许我们自定义WMTS瓦片计算方式。
WMTS和TilingScheme
首先我们来了解下WMTS瓦片计算方式,然后看下如何自定义一个自己的TilingScheme
对象,来扩充Cesium的多坐标系支持。OGC官网OpenGIS Web Map Tile Service Implementation Standard - Open Geospatial Consortium (ogc.org)讲的很清楚,最重要的就是下面这张图,图中标出来的属性就是WMTS的WMTS的Capabilities.xml的属性。
我们在看下WMTS瓦片计算公式,其实OGC提供的这个规范也给出来了:
pixelSpan = scaleDenominator × 0.28 10^-3 / metersPerUnit(crs)
tileSpanX = tileWidth × pixelSpan
tileSpanY = tileHeight × pixelSpan
tileMatrixMaxX = tileMatrixMinX + tileSpanX × matrixWidth
tileMatrixMinY = tileMatrixMaxY - tileSpanY × matrixHeight
scaleDenominator
大家尤其要注意下这个字段,它代表了实际尺寸和像素尺寸的比例,计算的时候不能忽略。
我们来结合一份实际的WMTS服务,来看看具体这些公式是什么意思。
<ows:BoundingBox crs="urn:ogc:def:crs:EPSG::4526">
<ows:LowerCorner>2705867.630193442 3.841100167045193E7</ows:LowerCorner>
<ows:UpperCorner>2776804.1782085816 3.849748247676485E7</ows:UpperCorner>
</ows:BoundingBox>
<TileMatrixSet>
<ows:Title>TileMatrix using 0.28mm</ows:Title>
<ows:Abstract>The tile matrix set that has scale values calculated based on the dpi defined by OGC specification (dpi assumes 0.28mm as the physical distance of a pixel).</ows:Abstract>
<ows:Identifier>default028mm</ows:Identifier>
<ows:SupportedCRS>urn:ogc:def:crs:EPSG::4526</ows:SupportedCRS>
<TileMatrix>
<ows:Identifier>0</ows:Identifier>
<ScaleDenominator>944942.3660750897</ScaleDenominator>
<TopLeftCorner>1.00021E7 3.28768E7</TopLeftCorner>
<TileWidth>256</TileWidth>
<TileHeight>256</TileHeight>
<MatrixWidth>83</MatrixWidth>
<MatrixHeight>108</MatrixHeight>
</TileMatrix>
</TileMatrixSet>
- 这是我用ArcGIS Server发布的4526坐标系的MWS部分内容,第一段是说明这个是4526坐标系,注意,在
<ows:LowerCorner>2705867.630193442 3.841100167045193E7</ows:LowerCorner>
这里,用的是高斯坐标系,也就是x是向北的,y是向东的,和我们的笛卡尔坐标系刚好反过来。 - 其中
TopLeftCorner
就是在你WMTS切片坐标系下的左上角顶点坐标,注意WMTS的坐标系,对于瓦片的tx
ty
来说,原点在左上角,向右是tx
,向下是ty
。 - 当我想要根据瓦片的
tx
ty
来计算实际坐标x
y
,我只用根据公式算出tileSpanY
tileSpanX
,然后加上这个TopLeftCorner
就是真实坐标了。同理,当我想要根据实际坐标x
y
来计算瓦片的tx
ty
时,首先和TopLeftCorner
作差,然后除以tileSpanY
tileSpanX
,在向下取整,就得出瓦片的坐标了。
ArcGIS MapServer服务
对于ArcGis 的MapServer来说,有两种办法,第一种参考Cesium的ArcGisMapServerImageryProvider
写一个自己的Provider,ArcGIS的瓦片计算规则和WMTS的类似,第二种就是用WMTS的方法来加,因为ArcGIS Server也可以发布WMTS服务。
代码
还以刚才那个4526坐标系的WMTS服务为例,我们看看如何写一个自己的TilingScheme
。首先来看下TilingScheme
的定义。首先是字段:
-
ellipsoid : Ellipsoid
,这个好说,WGS84或者CGCS2000 -
projection : MapProjection
这个需要自己写,就是在WMTS坐标系和WGS84坐标系下的转换,有两个成员函数project
unproject
。 -
rectangle : Rectangle
,需要提供在WGS84坐标系下的切片范围,在WMTS的xml文件里可以找到。
然后是成员函数: -
getNumberOfXTilesAtLevel(level) → Number
,根据zoom级别返回x
方向上的瓦片数量,WMTS的xm文件里也可以获取到 -
getNumberOfYTilesAtLevel(level) → Number
,根据zoom级别返回y
方向上的瓦片数量,WMTS的xm文件里也可以获取到 -
rectangleToNativeRectangle(rectangle, result) → Rectangle
,将WMTS指定坐标系下的Rectangle
转到WGS84坐标系下 -
tileXYToNativeRectangle(x, y, level, result) → Rectangle
,根据瓦片的坐标和层级,返回在WMTS指定坐标下的瓦片范围 -
tileXYToRectangle(x, y, level, result) → Rectangle
,根据瓦片的坐标和层级,返回在WGS84坐标下的瓦片范围
MapProjection
首先我们先来实现MapProjection,需要借助Proj4js这个库。
// 定义4526坐标系,可以在epsg.io上查
proj4.defs("EPSG:4526", "+proj=tmerc +lat_0=0 +lon_0=114 +k=1 +x_0=38500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +type=crs");
// 定义自己的MapProjection
class MapProjection {
static transform = proj4("EPSG:4326", "EPSG:4526");
constructor() {
this.ellipsoid = Cesium.Ellipsoid.WGS84;
}
project(cartographic, result) {
let lon = Cesium.Math.toDegrees(cartographic.longitude);
let lat = Cesium.Math.toDegrees(cartographic.latitude);
// 这里要注意,EPSG规定是纬度在前,经度在后,加上true就表示强制按经纬度的顺序来
const coord = MapProjection.transform.forward([lon, lat], true);
if (!Cesium.defined(result)) {
result = new Cesium.Cartesian3();
}
result.x = coord[0];
result.y = coord[1];
result.z = cartographic.height;
return result;
}
unproject(cartesian, result) {
const coord = MapProjection.transform.inverse([cartesian.x, cartesian.y]);
const lon = Cesium.Math.toRadians(coord[0]);
const lat = Cesium.Math.toRadians(coord[1]);
if (!Cesium.defined(result)) {
result = new Cesium.Cartographic();
}
console.log(coord);
result.longitude = lon;
result.latitude = lat;
// result.longitude = coord[0];
// result.latitude = coord[1];
result.height = cartesian.z;
return result;
}
}
TilingScheme
class ArcGISTilingScheme {
static scales = [944942.3660750897, 472471.18303754483, 236235.59151877242, 118117.79575938621, 60476.31142880573];
static xtiles = [83, 166, 332, 664, 1297];
static ytiles = [108, 216, 431, 862, 1684];
constructor() {
this.TopCorner = 1.00021E7;
this.LeftCorner = 3.28768E7;
this.ellipsoid = Cesium.Ellipsoid.WGS84;
this.projection = new MapProjection();
// 这个信息可以在wmts服务那里找到
this.rectangle = new Cesium.Rectangle(
Cesium.Math.toRadians(113.11773979064854),
Cesium.Math.toRadians(24.454099600244135),
Cesium.Math.toRadians(113.97516978692987),
Cesium.Math.toRadians(25.09704275817674));
}
getNumberOfXTilesAtLevel(level) {
return ArcGISTilingScheme.xtiles[level];
}
getNumberOfYTilesAtLevel(level) {
return ArcGISTilingScheme.ytiles[level];
}
positionToTileXY(position, level, result) {
let cart = this.projection.project(position);
const tileY = Math.floor((this.TopCorner - cart.y) / this.pixelSpan(level) / 256);
const tileX = Math.floor((cart.x - this.LeftCorner) / this.pixelSpan(level) / 256);
if (!Cesium.defined(result)) {
result = new Cesium.Cartesian2();
}
result.x = tileX;
result.y = tileY;
return result;
}
rectangleToNativeRectangle(rectangle, result) {
let coord1 = new Cesium.Cartographic(rectangle.west, rectangle.south, 0);
let coord2 = new Cesium.Cartographic(rectangle.east, rectangle.north, 0);
coord1 = this.projection.project(coord1);
coord2 = this.projection.project(coord2);
if (!Cesium.defined(result)) {
result = new Cesium.Rectangle();
}
result.west = coord1.x;
result.south = coord1.y;
result.east = coord2.x;
result.north = coord2.y;
return result;
}
tileXYToNativeRectangle(x, y, level, result) {
let xmin = this.pixelSpan(level) * 256 * x + this.LeftCorner;
let xmax = this.pixelSpan(level) * (x + 1) * 256 + this.LeftCorner;
let ymin = this.TopCorner - (y + 1) * 256 * this.pixelSpan(level);
let ymax = this.TopCorner - (y) * 256 * this.pixelSpan(level);
if (!Cesium.defined(result)) {
result = new Cesium.Rectangle();
}
result.west = xmin;
result.south = ymin;
result.east = xmax;
result.north = ymax;
return result;
}
tileXYToRectangle(x, y, level, result) {
const rectangle = this.tileXYToNativeRectangle(x, y, level);
let coord1 = new Cesium.Cartesian3(rectangle.west, rectangle.south, 0);
let coord2 = new Cesium.Cartesian3(rectangle.east, rectangle.north, 0);
coord1 = this.projection.unproject(coord1);
coord2 = this.projection.unproject(coord2);
if (!Cesium.defined(result)) {
result = new Cesium.Rectangle();
}
result.west = coord1.longitude;
result.south = coord1.latitude;
result.east = coord2.longitude;
result.north = coord2.latitude;
return result;
}
pixelSpan(level) {
return ArcGISTilingScheme.scales[level] * 0.28 / 1000.0;
}
}
应用
应用上就非常简单了,只用加上tilingScheme就好了。
let wmts = new Cesium.WebMapTileServiceImageryProvider({
url: 'http://192.168.72.193:6080/arcgis/rest/services/sg/MapServer/WMTS',
layer: 'sg',
format: "image/png",
style: 'default',
tileMatrixSetID: "default028mm",
maximumLevel: 4,
tilingScheme: new ArcGISTilingScheme(),
rectangle: new Cesium.Rectangle(
Cesium.Math.toRadians(113.11773979064854),
Cesium.Math.toRadians(24.454099600244135),
Cesium.Math.toRadians(113.97516978692987),
Cesium.Math.toRadians(25.09704275817674))
});
viewer.imageryLayers.addImageryProvider(wmts);