下載地圖瓦片的第一步,就是要計算出要下載哪些地圖瓦片。根據上篇內容,我們瞭解了谷歌瓦片組織的理論知識,現在就需要寫代碼實現這些內容。
一般情況下,我們會選擇一個矢量面文件作爲下載的範圍,需要計算出這個矢量面數據覆蓋了哪些瓦片,並存儲起來。存儲的時候,需要記錄每個瓦片的x、y和z,分別代表在x方向上的瓦片索引、y方向上的瓦片索引以及級別。
最開始的時候,我是使用xml文件記錄這些數據,但後面繼續開發的時候,老覺得當瓦片數據非常大的時候,例如當瓦片有幾十萬條的時候,xml需要一次性存儲和打開,此時可能會有性能問題。其次xml數據不直觀,不能直接展示每個瓦片的位置和範圍。所以最終決定用Shape文件存儲瓦片。Shape文件的定義規則如下。
1、面Shape文件,空間參考爲WGS1984SphereWebMercator;
2、字段包含x、y和z都是int類型,分別代表在x方向上的瓦片索引、y方向上的瓦片索引以及級別。
因爲方便,我們依然在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的最主要的原因。
我們根據中國範圍,生成了4-6級的瓦片信息,生成的結果和中國範圍數據一起在ArcMap中打開,如下圖所示。
上面的圖我們看着可能比較亂,因爲4-6級別的瓦片都混合到一起顯示的,下面我們只顯示第6級的瓦片,看下效果。