地图下载器 002 根据下载范围获取要下载的瓦片信息

1、瓦片信息的存储方式设计

下载地图瓦片的第一步,就是要计算出要下载哪些地图瓦片。根据上篇内容,我们了解了谷歌瓦片组织的理论知识,现在就需要写代码实现这些内容。

一般情况下,我们会选择一个矢量面文件作为下载的范围,需要计算出这个矢量面数据覆盖了哪些瓦片,并存储起来。存储的时候,需要记录每个瓦片的x、y和z,分别代表在x方向上的瓦片索引、y方向上的瓦片索引以及级别。

最开始的时候,我是使用xml文件记录这些数据,但后面继续开发的时候,老觉得当瓦片数据非常大的时候,例如当瓦片有几十万条的时候,xml需要一次性存储和打开,此时可能会有性能问题。其次xml数据不直观,不能直接展示每个瓦片的位置和范围。所以最终决定用Shape文件存储瓦片。Shape文件的定义规则如下。

1、面Shape文件,空间参考为WGS1984SphereWebMercator;

2、字段包含x、y和z都是int类型,分别代表在x方向上的瓦片索引、y方向上的瓦片索引以及级别。

2、下载的瓦片计算实现

1、获取下载范围的外包矩形框,得到左上角和右下角的坐标值。

因为方便,我们依然在ArcObejcts SDK的基础上开发,如果大家不想依赖ArcObejcts SDK,可以基于开源的 DotSpatial或者SharpMap都可以。首先我们打开下载范围的Shape文件,获取数据的外包矩形,进而得到左上和右下角的坐标值。

Type myType = Type.GetTypeFromProgID("esriDataSourcesFile.ShapefileWorkspaceFactory"); object myObject = Activator.CreateInstance(myType); IWorkspaceFactory myWorkspaceFactory = myObject as IWorkspaceFactory; IFeatureWorkspace myFeatureWorkspace = myWorkspaceFactory.OpenFromFile(System.IO.Path.GetDirectoryName(pShapeFilePath), 0) as IFeatureWorkspace; IFeatureClass myFeatureClass = myFeatureWorkspace.OpenFeatureClass(System.IO.Path.GetFileNameWithoutExtension(pShapeFilePath)); var myEnvelope = (myFeatureClass as IGeoDataset).Extent;

2、根据级别和左上右下坐标获取要下载的瓦片信息,并保存成Shape文件。

有了左上和右下的坐标信息,就可以根据级别计算要下载哪些瓦片了。我们先创建Shape文件,创建代码如下。

string myTileShapeFile = Framework.Helpers.FilePathHelper.GetTempShapeFilePath(); List<IField> myFieldList = new List<IField>(); var myWorldMercator = Framework.Helpers.SpatialReferenceHepler.GetBySRProjCSType((int)esriSRProjCS2Type.esriSRProjCS_WGS1984SphereWebMercator); myFieldList.Add(Framework.Helpers.FieldHelper.CreateShapeField(esriGeometryType.esriGeometryPolygon, myWorldMercator)); myFieldList.Add(Framework.Helpers.FieldHelper.CreateField("X", typeof(int))); myFieldList.Add(Framework.Helpers.FieldHelper.CreateField("Y", typeof(int))); myFieldList.Add(Framework.Helpers.FieldHelper.CreateField("Z", typeof(int))); IFeatureClass myTileFeatureClass = Framework.Helpers.ShapeFileHelper.CreateShapeFile(myTileShapeFile, myFieldList);

创建后,根据范围坐标,循环等级,得到各等级的瓦片信息,写入到Shape文件中。

double myMinLng = pEnvelope.XMin; double myMinLat = pEnvelope.YMin; double myMaxLng = pEnvelope.XMax; double myMaxLat = pEnvelope.YMax;  int myK = 0; IFeatureBuffer myFeatureBuffer = myTileFeatureClass.CreateFeatureBuffer(); IFeatureCursor myFeatureCursor = myTileFeatureClass.Insert(true); foreach (int myZoom in pZoomList) {     //将第一个点经纬度转换成平面2D坐标,左上点和右下点     var myLTPoint = this.LngLatToPixel(myMinLng, myMaxLat, myZoom);     var myRBPoint = this.LngLatToPixel(myMaxLng, myMinLat, myZoom);     int myStartColumn = (int)(myLTPoint.X / 256);  //起始列     int myEndColumn = (int)(myRBPoint.X / 256);   //结束列     if (myEndColumn == Math.Pow(2, myZoom))  //结束列超出范围     {         myEndColumn--;     }     int myStartRow = (int)(myLTPoint.Y / 256);  //起始行     int myEndRow = (int)(myRBPoint.Y / 256);   //结束行     if (myEndRow == Math.Pow(2, myZoom))  //结束行超出范围     {         myEndRow--;     }      int myTotalTileCount = (myEndRow - myStartRow + 1) * (myEndColumn - myStartColumn + 1);     for (int i = myStartRow; i <= myEndRow; i++)     {         for (int j = myStartColumn; j <= myEndColumn; j++)         {             myFeatureBuffer.Shape = this.RowColumnToMeter(i, j, myZoom);             myFeatureBuffer.Value[2] = j;             myFeatureBuffer.Value[3] = i;             myFeatureBuffer.Value[4] = myZoom;             myFeatureCursor.InsertFeature(myFeatureBuffer);             myK++;             if (myK % 1000 == 0)             {                 myFeatureCursor.Flush();                 string myProcessMessage = "正在创建第{Zoom}级瓦片,{K}/{TotalTileCount}"                     .Replace("{Zoom}", myZoom.ToString())                    .Replace("{K}", myK.ToString())                    .Replace("{TotalTileCount}", myTotalTileCount.ToString());                 pProcessInfo.SetProcess(myProcessMessage);             }         }     } } if (myK % 1000 > 0) {     myFeatureCursor.Flush(); } ComReleaser.ReleaseCOMObject(myFeatureCursor); Framework.Helpers.FeatureClassHelper.ReleaseFeatureClass(myTileFeatureClass); return myTileShapeFile;

该代码中有两个调用函数,一个是根据缩放级别zoom 将经纬度坐标系统中的某个点 转换成平面2D图中的点,另外一个是把行列转换成以米为单位的多边形。两个函数的定义如下。

/// <summary> /// 根据缩放级别zoom  将经纬度坐标系统中的某个点 转换成平面2D图中的点(原点在屏幕左上角) /// </summary> /// <param name="pLng"></param> /// <param name="pLat"></param> /// <param name="pZoom"></param> /// <returns></returns> private IPoint LngLatToPixel(double pLng, double pLat, double pZoom) {     double myCenterPoint = Math.Pow(2, pZoom + 7);     double myTotalPixels = 2 * myCenterPoint;     double myPixelsPerLngDegree = myTotalPixels / 360;     double myPixelsPerLngRadian = myTotalPixels / (2 * Math.PI);     double mySinY = Math.Min(Math.Max(Math.Sin(pLat * (Math.PI / 180)), -0.9999), 0.9999);     var myPoint = new PointClass     {         X = (int)Math.Round(myCenterPoint + pLng * myPixelsPerLngDegree),         Y = (int)Math.Round(myCenterPoint - 0.5 * Math.Log((1 + mySinY) / (1 - mySinY)) * myPixelsPerLngRadian)     };     return myPoint; } /// <summary> /// 把行列转换成以米为单位的多边形 /// </summary> /// <param name="pRowIndex"></param> /// <param name="pColumnIndex"></param> /// <param name="pZoom"></param> /// <returns></returns> private IPolygon RowColumnToMeter(int pRowIndex, int pColumnIndex, int pZoom) {     double myL = 20037508.3427892;     double myA = Math.Pow(2, pZoom);     double myMinX = -myL + pColumnIndex * myL * 2 / myA;     double myMaxX = -myL + (pColumnIndex + 1) * myL * 2 / myA;     double myMinY = myL - (pRowIndex + 1) * myL * 2 / myA;     double myMaxY = myL - (pRowIndex) * myL * 2 / myA;     var myPolygon = new PolygonClass();     myPolygon.AddPoint(new PointClass() { X = myMinX, Y = myMinY });     myPolygon.AddPoint(new PointClass() { X = myMinX, Y = myMaxY });     myPolygon.AddPoint(new PointClass() { X = myMaxX, Y = myMaxY });     myPolygon.AddPoint(new PointClass() { X = myMaxX, Y = myMinY });     myPolygon.AddPoint(new PointClass() { X = myMinX, Y = myMinY });     return myPolygon; }

此时的结果是根据下载范围的外包矩形计算出来的,所以我们还要根据下载图形对计算出的瓦片信息矢量数据进行裁切。调用ArcObjects SDK中的SpatialJoin工具,把下载范围包含以及相交的瓦片都保留下来,生成一个新的瓦片shape文件,代码如下。

var mySpatialJoin = new SpatialJoin {     target_features = myTileShapeFile,     join_features = this.RangShapeFilePath,     join_type = "KEEP_COMMON",     out_feature_class = FilePathHelper.GetTempShapeFilePath() }; var myGPEx = new GPEx(); myGPEx.ExecuteByGP(mySpatialJoin); myTileShapeFile = mySpatialJoin.out_feature_class.ToString(); //把裁切后的文件拷贝到指定目录下 ShapeFileHelper.Copy(myTileShapeFile, this.TileShapeFile);

在ArcObjects SDK中做这步操作比较简单,如果自己使用C#实现,或者调用其他库去实现,会麻烦些,这也是使用ArcObjects SDK的最主要的原因。

这步操作完,瓦片信息数据的生成就完成了。

3、瓦片信息生成结果

我们根据中国范围,生成了4-6级的瓦片信息,生成的结果和中国范围数据一起在ArcMap中打开,如下图所示。

地图下载器 002 根据下载范围获取要下载的瓦片信息

我们看下该数据包含的属性信息,如下图所示。

地图下载器 002 根据下载范围获取要下载的瓦片信息

上面的图我们看着可能比较乱,因为4-6级别的瓦片都混合到一起显示的,下面我们只显示第6级的瓦片,看下效果。

地图下载器 002 根据下载范围获取要下载的瓦片信息

这样看着就清晰多了 ,把行列号按照x,y格式显示到地图上,效果如下图所示。

地图下载器 002 根据下载范围获取要下载的瓦片信息

接下来,我们就可以循环瓦片信息Shape文件中的要素,一个个下载瓦片了。

发表评论

相关文章