1.背景
19年上半年,中南电力设计院有个“配电网数字化设计平台”项目,记得当时需要支持天地图、Esri、Google等的影像地图,并且支持离线使用,这里就介绍下怎么在ArcEngine中使用天地图的。为什么说是当时呢?因为后来没有做这个事情了。
2.计算图层层级
加载网络地图的核心无非就是两个点:计算图层层级,计算图层瓦片的行列号。
在线地图瓦片模型使用的一般都是金字塔模型,每一个层级的分辨率都是固定的。对于一个任意的分辨率,我们需要计算出其在已有的瓦金字塔模型中对应的最合适的分辨率。天地图所提供的在线地图的各个层级分辨率,可以通过其WMTS服务API进行查看,例如经纬度投影影像底图的元数据信息为
https://t0.tianditu.gov.cn/img_c/wmts?request=GetCapabilities&service=wmts&tk=您的密钥
假设输入的分辨率为rsrsrs,金字塔各个层级分辨率集合为为a,2a,4a,…,2na{a, 2a, 4a, …, 2^na}a,2a,4a,…,2na,则通过寻找层级n,使得满足条件rs>(2n−1a+2na)2rs>\frac{(2^{n-1}a+2^na)}{2}rs>2(2n−1a+2na) 且 rs≤(2na+2n+a)2rs\leq\frac{(2^{n}a+2^{n+}a)}{2}rs≤2(2na+2n+a),则该n为实际绘制电子地图所使用的层级,2na2^na2na为实际绘制电子地图所使用的比例尺。写成代码大致如下:
1 | `protected virtual int CalcLevel(double realScale, ref double adjustScale) { int z = 18; double min = 0; int zoom = 0; for (int i = 0; i < Scales.Length - 1; i++) { if (i != 0) min = Scales[i - 1] + ((Scales[i] - Scales[i - 1]) / 2); var max = Scales[i] + ((Scales[i + 1] - Scales[i]) / 2); if (realScale > min && realScale <= max) { adjustScale = Scales[i]; zoom = z; return zoom < 1 ? 1 : zoom; } z--; } return zoom; } ` |
3.计算图层瓦片的行列号
电子地图在绘制时有个矩形显示范围,根据这个范围可以得到四个值:minX、minY、maxX、maxY。根据天地图的金字塔瓦片模型的切片方案,可以知道对于第n层,其有2^n×2^n个瓦片,行列号从左上角开始编号。所以其解算的代码大致如下:
1 | `protected virtual ViewTilesInfo GetViewTilesInfo(int level, IEnvelope ext) { if (level < 1) { level = 1; } if (level > 18) { level = 18; } var upperLeft = ext.UpperLeft; var lowerRight = ext.LowerRight; if (upperLeft.SpatialReference is IProjectedCoordinateSystem proj) { upperLeft.Project(proj.GeographicCoordinateSystem); lowerRight.Project(proj.GeographicCoordinateSystem); } var left = upperLeft.X; var top = upperLeft.Y; var right = lowerRight.X; var bottom = lowerRight.Y; var maxNum = Math.Pow(2, level); var width = 360 / maxNum; var height = 180 / maxNum; var minX = (int)((left + 180) / width); var minY = (int) ((90 - top) / height); var maxX = (int)((right + 180) / width); var maxY = (int)((90 - bottom) / height); var maxRowCol = (int)maxNum - 1; return new ViewTilesInfo() { Level = level, Width = width, Height = height, MinCol = minX < 0 ? 0 : minX, MinRow = minY < 0 ? 0 : minY, MaxCol = maxX > maxRowCol ? maxRowCol : maxX, MaxRow = maxY > maxRowCol ? maxRowCol : maxY }; } ` |
4.瓦片请求与渲染
计算得到所要渲染的瓦片所在的层级、行列号后,即可直接通过HTTP请求获取瓦片。代码如下:
1 | `protected virtual Image GetImages(int x, int y, int z) { if (x < 0 || y < 0 || z < 1 || z > 18) { return null; } var url = BaseUrl.Replace("{x}", x.ToString()) .Replace("{y}", y.ToString()) .Replace("{z}", z.ToString()); byte[] bytes; using (WebClient wc = new WebClient()) { bytes = wc.DownloadData(url); } if (bytes.Length > 0) { MemoryStream ms = new MemoryStream(bytes); return Image.FromStream(ms); } return null; } ` |
其实,由于需求要求能够在外网、内网下同时使用,这里是使用Nancy进行反向代理,没有就先下载到本地存储,有就直接返回图片。Nancy目前也已经不再维护了。
图层的渲染是发生在Layer.Draw这个方法里面的。将其中具体绘制的方法抽离如下:
1 | `/// /// 具体用于绘制地图瓦片的方法 /// /// protected virtual void DrawTiles(IDisplay Display) { try { //获取绘制的钩子 Display.StartDrawing(Display.hDC, 0); IntPtr hdc = new IntPtr(Display.hDC); Graphics gc = Graphics.FromHdc(hdc); //当前显示范围 IEnvelope ext = this._mapControl.ActiveView.Extent; //当前应该显示的瓦片的层级、最大最小行列号 var viewTilesInfo = GetViewTilesInfo(_level, ext); var level = viewTilesInfo.Level; MapControlHelper mapControlHelper = new MapControlHelper(this._mapControl); var startGeoXy = LayerHelper.Xyz2GeoCoord(level, viewTilesInfo.MinCol, viewTilesInfo.MinRow); var startPixelXy = mapControlHelper.Geo2Pixel(startGeoXy); #region 先进行图像的拼接 var imageWNum = viewTilesInfo.MaxCol - viewTilesInfo.MinCol + 1; var imageHNum = viewTilesInfo.MaxRow - viewTilesInfo.MinRow + 1; var resultImgW = imageWNum * 256; var resultImgH = imageHNum * 256; Image resultImage = new Bitmap(resultImgW, resultImgH); Graphics resultGraphics = Graphics.FromImage(resultImage); int xPixel = 0; for (int x = viewTilesInfo.MinCol; x <= viewTilesInfo.MaxCol; x++) { int yPixel = 0; for (int y = viewTilesInfo.MinRow; y <= viewTilesInfo.MaxRow; y++) { var image = GetImages(x, y, level) ?? nodata; resultGraphics.DrawImage(image, new Rectangle(xPixel, yPixel, 256, 256)); yPixel += 256; } xPixel += 256; } resultGraphics.Dispose(); var endGeoXy = LayerHelper.Xyz2GeoCoord(level, viewTilesInfo.MaxCol + 1, viewTilesInfo.MaxRow + 1); var endPixelXy = mapControlHelper.Geo2Pixel(endGeoXy); #endregion gc.DrawImage(resultImage, new RectangleF(startPixelXy[0], startPixelXy[1], endPixelXy[0] - startPixelXy[0], endPixelXy[1] - startPixelXy[1])); gc.Dispose(); } catch (Exception ex) { _errInfo = ex.Message; } } ` |
在接口的实现,即Layer.Draw方法中对DrawTiles方法进行调用即可。
1 | `void ILayer.Draw(esriDrawPhase DrawPhase, IDisplay Display, ITrackCancel TrackCancel) { if (DrawPhase == esriDrawPhase.esriDPGeography) { var curMpaScale = _mapControl.MapScale; //curMpaScale<_lastScale:放大 if (Math.Abs(_lastScale - curMpaScale) > 1e-6) { ReCalcScale(); _lastScale = curMpaScale; } //这里可以做个内存缓存 DrawTiles(Display); Display.FinishDrawing(); _needRedrawForgroud = true; } } ` |
可以看到,DrawTiles上方还有注释,可以通过内存缓存瓦片,减少瓦片获取的HTTP请求。
5.WebNetworkLayer
ESRI.ArcGIS.Carto.ILayer是AE开发中的所有类型的图层都要实现的一个借口,用以获取或设置图层的属性,包括图层名、图层的范围等各种元数据。如果要自定义图图层,ESRI.ArcGIS.Carto.ILayer的实现是必不可少的。
创建抽象类WebNetworkLayer,并实现ESRI.ArcGIS.Carto.ILayer接口,并将前述内容进行封装,对于WebNetworkLayer的实现基本完成了,对于不同种类的影像请求,直接继承WebNetworkLayer即可。
事实上,不同商家的遥感影像切片方案不同,另外一种常见的方案是将第一层分割为1行2列的瓦片,之后每一层级在上一层的基础上再进行2×2的切割,可以将切片方案抽离为单独的类,WebNetworkLayer实例化是进行构造即可。
1 | `// 由于进行了反向代理,所以请求地址都是localhost:8099 class TdtLayer:WebNetworkLayer { public TdtLayer(AxMapControl mapControl, string baseUrl= "http://localhost:8099/tdt/{z}/{y}/{x}", string name = "天地图") : base(mapControl, baseUrl, name) { } } class GoogleLayer : WebNetworkLayer { public GoogleLayer(AxMapControl mapControl, string baseUrl= "http://localhost:8099/google/{z}/{y}/{x}", string name = "Google地图") : base(mapControl, baseUrl, name) { } } class ArcgisOnline : WebNetworkLayer { public ArcgisOnline(AxMapControl mapControl, string baseUrl= "http://localhost:8099/arcgis/{z}/{y}/{x}", string name = "ArcGISOnline") : base(mapControl, baseUrl, name) { } } ` |
使用时,只需实例化对于的地图,并将其添加到MapControl控件中即可。
1 | `var layer = new TdtLayer(this.mainMapControl); this.mainMapControl.AddLayer(_tdtLayer); this.mainMapControl.Refresh(); ` |
由于没有装ArcGIS,所以就没有效果图可看了。以前每次重装了系统,装ArcGIS都是第一梯队的事情,现在毕业了,终于不必如此了。
参考:https://blog.csdn.net/mailtogst/article/details/6852798?spm=1001.2014.3001.5501