地圖下載器 002 根據下載範圍獲取要下載的瓦片資訊

2023-01-28 12:00:24

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中開啟,如下圖所示。

截圖.png

我們看下該資料包含的屬性資訊,如下圖所示。

截圖.png

上面的圖我們看著可能比較亂,因為4-6級別的瓦片都混合到一起顯示的,下面我們只顯示第6級的瓦片,看下效果。

截圖.png

這樣看著就清晰多了 ,把行列號按照x,y格式顯示到地圖上,效果如下圖所示。

截圖.png

接下來,我們就可以迴圈瓦片資訊Shape檔案中的要素,一個個下載瓦片了。