ArcGIS Engine,導入數據的幾種方式及其效率對比

在ArcGIS Engine開發過程中,比較常用並且重要的功能就是數據轉換,對於數據轉換方法您是否足夠清楚?ArcGIS Engine中常用的數據轉換方法有哪些?各種轉換方法的優缺點是什麼?採用哪種方法效率更高?如果您對這些問題感興趣,那麼一定要閱讀下面文章,相信一定會讓您有所收穫。

一、ArcGIS Engine中導入數據的幾種方式及其優缺點:

IFeatureDataConverter:細粒度,用於複製單個簡單要素類或者要素數據集。優點:可使用QueryFilter(屬性和空間均可)進行過濾,可以設置SubFields指定複製哪些字段,可以改變要素類的geometry definition以及設置configuration_keyword。除此之外,還可以在基於文件的數據源(如ShapeFile)與地理數據庫之間進行復制。缺點:只能轉換簡單要素類和要素數據集,無法轉換關係類,幾何網絡,拓撲等複雜對象。

IGeoDBDataTransfer(等同於ArcCatalog中對地理數據庫中數據集的複製和粘貼):適用於兩個地理數據庫之間複製一個或多個數據集。優點:可以一次導入多個數據集,並且能轉換幾乎所有類型的數據,包括關係類、拓撲、幾何網絡等複雜對象。缺點:不能進行條件過濾,也不能在基於文件的數據源與地理數據庫之間進行復制,即不能實現shapefile與FileGDB、MDB或SDE間的導入導出。

IGdbXmlExport and IGdbXmlImport:使用XML作爲中間文件,適用於兩個地理數據庫之間複製一個或多個數據集。優點:可以在離線下使用。比如SDE往其它地理數據庫中進行數據導入,在連接上SDE時生成xml文件,後面即使斷開該SDE連接,也可以成功將xml(可僅包括Schema,也可以包含數據)導入到目標庫中。還有一個優點是便於數據共享與傳輸,如果給多個人共享數據,只需拷貝該xml文件即可。

IExportOperation:複製單個數據集到另一個Workspace,是經過包裝的IFeatureDataConverter。優點:可以設置QueryFilter以及SelectionSet,可以跨數據源進行轉換,比如從ShapeFile到GeoDatabase,並且可以顯示進度條。缺點:只能在Desktop產品下使用。

IDataset.Copy:從基於文件的數據源(例如shapefile,dbf或coverage要素類)中複製Dataset到另一個基於文件的數據源。

IWorkspaceFactory(copy或者move):複製或移動整個local geodatabase或SDE 連接文件。

IObjectLoader:往已有數據集中添加記錄,僅能在Desktop產品下使用。由於本文僅討論複製要素類的情況,該接口我們將在下一篇關於Engine中如何往已有要素類中插入數據中討論。

當然,不要忘了還可以在程序中調用GP工具進行要素類的複製,比如FeatureClassToFeatureClass工具、FeatureClassToGeoDatabase工具、CopyFeatures工具以及Copy工具。

二、效率對比

前面我們介紹了Engine中常用的複製要素類的幾種方法及每種方法的優缺點,接下來我們就進行測試,對比一下效率。本文以將FileGDB中含有20萬條記錄的點要素類導入SDE中爲例進行測試。

代碼可參考 ArcGIS幫助

Tips:如果程序中綁定Engine,則需要初始化EngineGeoDB許可(編輯SDE數據需要該許可,當然如果綁定Desktop,也可以使用Standard或者Advanced許可),直接使用Engine許可會報下面錯誤。

報錯信息

下面我們就看測試結果吧。

1, 使用IFeatureDataConverter.ConvertFeatureClass將FileGDB中要素類導入SDE中所用時間爲:3分鐘16秒。爲了便於對比,這裏沒有設置查詢條件以及SubFields等。

2,使用IGeoDBTransfer.Transfer將同樣數據導入SDE中所用時間爲:2分42秒,比第一種方法快34秒。效率提高了17%,目前我導入的數據量僅爲20萬,相信如果數據量再大些,該方法的效率會更突出。

3,使用IGdbXmlExport將FileGDB中的要素類導出成xml所用時間爲:1分55秒,接着使用IGdbXmlImport將生成的xml導入到SDE中所用時間爲:4分27秒。兩個時間加起來顯然並未提高效率,但是生成一次xml,後續可以將其導入任意地理數據庫中,便於傳輸與共享。

4,最後測試使用IExportOperation.ExportFeatureClass 方法,注意該接口只能在Desktop產品下使用,也就是程序中需要綁定Desktop並且初始化Standard或者Advanced許可。執行該方法時會自動出現進度條來顯示進度。

Export Progress

所用時間爲:3分22秒。可見該方法與IFeatureDataConverter所用時間大致相同,也就驗證了前面說的“IExportOperation是包裝過的IFeatureDataConverter”說法,如果您的程序中不需要使用MapControl、ToolBarControl等控件,並且需要顯示進度條,那麼使用這個方法最合適不過了。

其餘的IDataset.Copy,IWorkspaceFactory(copy或者move)以及IObjectLoader方法無法實現將FileGDB中的要素類導出到SDE中,所以這裏就不做討論了。接下來我們在程序中調用GP工具測試一下時間。

1,調用FeatureClassToFeatureClass工具,所用時間爲3分32秒。

2,調用FeatureClassToGeoDatabase工具,所用時間爲3分4秒。

3,調用CopyFeatures工具,所用時間爲3分25秒。

4,調用Copy工具,所用時間爲2分45秒。

從執行時間上可以發現Copy工具和FeatureClassToGeoDatabase工具用時較少,爲大家提供一個參考。

小結:執行相同操作所花時間的長短雖然是一個程序是否可優化的考量因素,但是最重要的是實際需求,比如IGeoDBDataTransfer雖然較IFeatureDataConverter用時短,但是如果想在複製過程中進行查詢或者設置SubFields,再或者要將shapeFile導入SDE中,那麼就只能使用IFeatureDataConverter了。

說了這麼多,前面的東西是否又忘了呢,不要緊,文章最後將之前提到的使用AO接口導入數據的方法彙總如下:

完整代碼:

 

using ESRI.ArcGIS.Catalog;
using ESRI.ArcGIS.DataSourcesGDB;
using ESRI.ArcGIS.esriSystem;
using ESRI.ArcGIS.Geodatabase;
using ESRI.ArcGIS.GeoDatabaseDistributed;
using ESRI.ArcGIS.GeoDatabaseUI;
using ESRI.ArcGIS.Geoprocessing;
using ESRI.ArcGIS.Geoprocessor;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace convertingData
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        
        string sdePath = @"C:\Users\Xinying\AppData\Roaming\ESRI\Desktop10.4\ArcCatalog\Connection to 192.168.220.132.sde";
        string fileGDBPath = Application.StartupPath + "\\test.gdb";
        string sourceFCName = "testPoint_20w";
        string targetFCName = "testPoint_20w_sde";
        string outputXmlFile = Application.StartupPath + "\\workspaceGDB.XML";
        
        private void iFeatureDataConverterToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Stopwatch myWatch = Stopwatch.StartNew();

            ConvertFileGDBToSDE(fileGDBPath, sourceFCName, targetFCName);

            myWatch.Stop();
            string time = myWatch.Elapsed.TotalMinutes.ToString();
            MessageBox.Show(time + " Minutes");         

           
        }
       
        public void ConvertFileGDBToSDE(string fileGDBPath, string sourceFCName, string targetFCName)
        {            
            // Create a name object for the source (fileGDB) workspace and open it.
            IWorkspaceName sourceWorkspaceName = getWorkspaceName("esriDataSourcesGDB.FileGDBWorkspaceFactory", fileGDBPath);

            IName sourceWorkspaceIName = (IName)sourceWorkspaceName;
            IWorkspace sourceWorkspace = (IWorkspace)sourceWorkspaceIName.Open();

            // Create a name object for the target (SDE) workspace and open it.
            IWorkspaceName targetWorkspaceName = getWorkspaceName("esriDataSourcesGDB.SdeWorkspaceFactory", sdePath);
            IName targetWorkspaceIName = (IName)targetWorkspaceName;
            IWorkspace targetWorkspace = (IWorkspace)targetWorkspaceIName.Open();

            // Create a name object for the source dataset.
            IFeatureClassName sourceFeatureClassName = getFeatureClassName(sourceWorkspaceName, sourceFCName);

            // Create a name object for the target dataset.
            IFeatureClassName targetFeatureClassName = getFeatureClassName(targetWorkspaceName, targetFCName);

            // Open source feature class to get field definitions.
            IName sourceName = (IName)sourceFeatureClassName;
            IFeatureClass sourceFeatureClass = (IFeatureClass)sourceName.Open();

            // Create the objects and references necessary for field validation.
            IFieldChecker fieldChecker = new FieldCheckerClass();
            IFields sourceFields = sourceFeatureClass.Fields;
            IFields targetFields = null;
            IEnumFieldError enumFieldError = null;

            // Set the required properties for the IFieldChecker interface.
            fieldChecker.InputWorkspace = sourceWorkspace;
            fieldChecker.ValidateWorkspace = targetWorkspace;

            // Validate the fields and check for errors.
            fieldChecker.Validate(sourceFields, out enumFieldError, out targetFields);
            if (enumFieldError != null)
            {
                // Handle the errors in a way appropriate to your application.
                Console.WriteLine("Errors were encountered during field validation.");
            }

            // Find the shape field.
            String shapeFieldName = sourceFeatureClass.ShapeFieldName;
            int shapeFieldIndex = sourceFeatureClass.FindField(shapeFieldName);
            IField shapeField = sourceFields.get_Field(shapeFieldIndex);

            // Get the geometry definition from the shape field and clone it.
            IGeometryDef geometryDef = shapeField.GeometryDef;
            IClone geometryDefClone = (IClone)geometryDef;
            IClone targetGeometryDefClone = geometryDefClone.Clone();
            IGeometryDef targetGeometryDef = (IGeometryDef)targetGeometryDefClone;

            // Cast the IGeometryDef to the IGeometryDefEdit interface.
            IGeometryDefEdit targetGeometryDefEdit = (IGeometryDefEdit)targetGeometryDef;

            // Set the IGeometryDefEdit properties.
            //targetGeometryDefEdit.GridCount_2 = 1;
            //targetGeometryDefEdit.set_GridSize(0, 0.75);

            // Create a query filter to only select features you want to convert
            IQueryFilter queryFilter = new QueryFilterClass();

            // Create the converter and run the conversion.
            IFeatureDataConverter featureDataConverter = new FeatureDataConverterClass();
            IEnumInvalidObject enumInvalidObject = featureDataConverter.ConvertFeatureClass
                (sourceFeatureClassName, queryFilter, null, targetFeatureClassName,
                targetGeometryDef, targetFields, "", 1000, 0);

            // Check for errors.
            IInvalidObjectInfo invalidObjectInfo = null;
            enumInvalidObject.Reset();
            while ((invalidObjectInfo = enumInvalidObject.Next()) != null)
            {
                // Handle the errors in a way appropriate to the application.
                Console.WriteLine("Errors occurred for the following feature: {0}",
                    invalidObjectInfo.InvalidObjectID);
            }
           
        }

        //"esriDataSourcesGDB.FileGDBWorkspaceFactory"
        //"esriDataSourcesFile.ShapefileWorkspaceFactory"
        //"esriDataSourcesGDB.SdeWorkspaceFactory"
        private IWorkspaceName getWorkspaceName(string WorkspaceFactoryProgID, string PathName)
        {
            IWorkspaceName workspaceName = new WorkspaceNameClass
            {
                WorkspaceFactoryProgID = WorkspaceFactoryProgID,
                PathName = PathName
            };
            return workspaceName;
        }
       
        private IFeatureClassName getFeatureClassName(IWorkspaceName WorkspaceName, string Name)
        {
            // Create a name object for the dataset.
            IFeatureClassName featureClassName = new FeatureClassNameClass();
            IDatasetName datasetName = (IDatasetName)featureClassName;
            datasetName.Name = Name;
            datasetName.WorkspaceName = WorkspaceName;
            return featureClassName;
        }

        private void iGeoDBTransferToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Stopwatch myWatch = Stopwatch.StartNew();
        
            TansferFileGDBToSDE(fileGDBPath, sourceFCName);
            
            myWatch.Stop();
            string time = myWatch.Elapsed.TotalMinutes.ToString();
            MessageBox.Show(time + " Minutes");
        }
        public void TansferFileGDBToSDE(string fileGDBPath, string sourceFCName)
        {
            // Create workspace name objects.
            IWorkspaceName sourceWorkspaceName = getWorkspaceName("esriDataSourcesGDB.FileGDBWorkspaceFactory", fileGDBPath);

            // Create a name object for the target (SDE) workspace and open it.
            IWorkspaceName targetWorkspaceName = getWorkspaceName("esriDataSourcesGDB.SdeWorkspaceFactory", sdePath);

            IName targetName = (IName)targetWorkspaceName;

            // Create a name object for the source dataset.
            IFeatureClassName sourceFeatureClassName = getFeatureClassName(sourceWorkspaceName, sourceFCName);
            IName sourceName = (IName)sourceFeatureClassName;

            // Create an enumerator for source datasets.
            IEnumName sourceEnumName = new NamesEnumeratorClass();
            IEnumNameEdit sourceEnumNameEdit = (IEnumNameEdit)sourceEnumName;

            // Add the name object for the source class to the enumerator.
            sourceEnumNameEdit.Add(sourceName);

            // Create a GeoDBDataTransfer object and a null name mapping enumerator.
            IGeoDBDataTransfer geoDBDataTransfer = new GeoDBDataTransferClass();
            IEnumNameMapping enumNameMapping = null;

            // Use the data transfer object to create a name mapping enumerator.
            Boolean conflictsFound = geoDBDataTransfer.GenerateNameMapping(sourceEnumName,
                targetName, out enumNameMapping);
            enumNameMapping.Reset();

            // Check for conflicts.
            if (conflictsFound)
            {
                // Iterate through each name mapping.
                INameMapping nameMapping = null;
                while ((nameMapping = enumNameMapping.Next()) != null)
                {
                    // Resolve the mapping's conflict (if there is one).
                    if (nameMapping.NameConflicts)
                    {
                        nameMapping.TargetName = nameMapping.GetSuggestedName(targetName);
                    }

                    // See if the mapping's children have conflicts.
                    IEnumNameMapping childEnumNameMapping = nameMapping.Children;
                    if (childEnumNameMapping != null)
                    {
                        childEnumNameMapping.Reset();

                        // Iterate through each child mapping.
                        INameMapping childNameMapping = null;
                        while ((childNameMapping = childEnumNameMapping.Next()) != null)
                        {
                            if (childNameMapping.NameConflicts)
                            {
                                childNameMapping.TargetName = childNameMapping.GetSuggestedName(targetName);
                            }
                        }
                    }
                }
            }

            // Start the transfer.
            geoDBDataTransfer.Transfer(enumNameMapping, targetName);
        }
        private void ExportDatasetToXML(string fileGdbPath, string sourceFCName, string outputXmlFile)
        {
            // Open the source geodatabase and create a name object for it.
            IWorkspaceName sourceWorkspaceName = getWorkspaceName("esriDataSourcesGDB.FileGDBWorkspaceFactory", fileGdbPath);

            IFeatureClassName sourceFeatureClassName = getFeatureClassName(sourceWorkspaceName, sourceFCName);
            IName sourceName = (IName)sourceFeatureClassName;

            // Create a new names enumerator and add the feature dataset name.
            IEnumNameEdit enumNameEdit = new NamesEnumeratorClass();
            enumNameEdit.Add(sourceName);
            IEnumName enumName = (IEnumName)enumNameEdit;          

            // Create a GeoDBDataTransfer object and create a name mapping.
            IGeoDBDataTransfer geoDBDataTransfer = new GeoDBDataTransferClass();
            IEnumNameMapping enumNameMapping = null;
            geoDBDataTransfer.GenerateNameMapping(enumName, sourceWorkspaceName as IName, out enumNameMapping);

            // Create an exporter and export the dataset with binary geometry, not compressed,
            // and including metadata.
            IGdbXmlExport gdbXmlExport = new GdbExporterClass();
            gdbXmlExport.ExportDatasets(enumNameMapping, outputXmlFile, true, false, true);
          
        }
        private void ImportXmlWorkspaceDocument(string inputXmlFile)
        {
            IWorkspaceName targetWorkspaceName = getWorkspaceName("esriDataSourcesGDB.SdeWorkspaceFactory", sdePath);
            IName targetName = (IName)targetWorkspaceName;
            IWorkspace targetWorkspace = (IWorkspace)targetName.Open();           

            IGdbXmlImport gdbXmlImport = new GdbImporterClass();
            IEnumNameMapping enumNameMapping = null;
            
            Boolean conflictsFound = gdbXmlImport.GenerateNameMapping(inputXmlFile, targetWorkspace, out enumNameMapping);

            // Check for conflicts.
            if (conflictsFound)
            {
                // Iterate through each name mapping.
                INameMapping nameMapping = null;
                enumNameMapping.Reset();
                while ((nameMapping = enumNameMapping.Next()) != null)
                {
                    // Resolve the mapping's conflict (if there is one).
                    if (nameMapping.NameConflicts)
                    {
                        nameMapping.TargetName = nameMapping.GetSuggestedName(targetName);
                    }

                    // See if the mapping's children have conflicts.
                    IEnumNameMapping childEnumNameMapping = nameMapping.Children;
                    if (childEnumNameMapping != null)
                    {
                        childEnumNameMapping.Reset();

                        // Iterate through each child mapping.
                        INameMapping childNameMapping = null;
                        while ((childNameMapping = childEnumNameMapping.Next()) != null)
                        {
                            if (childNameMapping.NameConflicts)
                            {
                                childNameMapping.TargetName =
                                    childNameMapping.GetSuggestedName(targetName);
                            }
                        }
                    }
                }
            }

            // Import the workspace document, including both schema and data.
            gdbXmlImport.ImportWorkspace(inputXmlFile, enumNameMapping, targetWorkspace, false);
        }
     
        private void exportDatasetToXMLImportToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Stopwatch myWatch = Stopwatch.StartNew();            
           
            ExportDatasetToXML(fileGDBPath, sourceFCName, outputXmlFile);
            myWatch.Stop();
            string time = myWatch.Elapsed.TotalMinutes.ToString();
            MessageBox.Show(time + " Minutes");

            Stopwatch myWatch1 = Stopwatch.StartNew();

            ImportXmlWorkspaceDocument(outputXmlFile);
            myWatch1.Stop();
            string time1 = myWatch1.Elapsed.TotalMinutes.ToString();
            MessageBox.Show(time1 + " Minutes"); 
    
        }

        private void iExportOperationToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Stopwatch myWatch = Stopwatch.StartNew();          
          
            ExportOperationFileGDBToSDE(fileGDBPath, sourceFCName, targetFCName);
            
            myWatch.Stop();
            string time = myWatch.Elapsed.TotalMinutes.ToString();
            MessageBox.Show(time + " Minutes");
        }

        public void ExportOperationFileGDBToSDE(string fileGDBPath, string sourceFCName, string targetFCName)
        {

            // Create a name object for the source (file GDB) workspace and open it.
            IWorkspaceName sourceWorkspaceName = getWorkspaceName("esriDataSourcesGDB.FileGDBWorkspaceFactory", fileGDBPath);

            IName sourceWorkspaceIName = (IName)sourceWorkspaceName;
            IWorkspace sourceWorkspace = (IWorkspace)sourceWorkspaceIName.Open();

            // Create a name object for the target (sde) workspace and open it.

            IWorkspaceName targetWorkspaceName = getWorkspaceName("esriDataSourcesGDB.SdeWorkspaceFactory", sdePath);
          
            // Create a name object for the source dataset.
            IFeatureClassName sourceFeatureClassName = getFeatureClassName(sourceWorkspaceName, sourceFCName);

            // Create a name object for the target dataset.
            IFeatureClassName targetFeatureClassName = getFeatureClassName(targetWorkspaceName, targetFCName);

            // Open source feature class to get field definitions.
            IName sourceName = (IName)sourceFeatureClassName;
            IFeatureClass sourceFeatureClass = (IFeatureClass)sourceName.Open();

            // Find the shape field.
            String shapeFieldName = sourceFeatureClass.ShapeFieldName;
            int shapeFieldIndex = sourceFeatureClass.FindField(shapeFieldName);
            IField shapeField = sourceFeatureClass.Fields.get_Field(shapeFieldIndex);

            // Get the geometry definition from the shape field and clone it.
            IGeometryDef geometryDef = shapeField.GeometryDef;
            IClone geometryDefClone = (IClone)geometryDef;
            IClone targetGeometryDefClone = geometryDefClone.Clone();
            IGeometryDef targetGeometryDef = (IGeometryDef)targetGeometryDefClone;

            // Cast the IGeometryDef to the IGeometryDefEdit interface.
            IGeometryDefEdit targetGeometryDefEdit = (IGeometryDefEdit)targetGeometryDef;

            // Set the IGeometryDefEdit properties.
            //targetGeometryDefEdit.GridCount_2 = 1;
            //targetGeometryDefEdit.set_GridSize(0, 0.75);

            // Create a query filter to only select features you want to convert
            IQueryFilter queryFilter = new QueryFilterClass();

            // Create the converter and run the conversion.
            IExportOperation exportOperation = new ExportOperationClass();
            exportOperation.ExportFeatureClass(sourceFeatureClassName as IDatasetName, queryFilter, null, targetGeometryDef, targetFeatureClassName, 0);

        }
        private void ExecuteGP(IGPProcess GPProcess)
        {
           
            Geoprocessor gp = new Geoprocessor { OverwriteOutput = true };           
            
            try
            {
                IGeoProcessorResult2 result = gp.Execute(GPProcess, null) as IGeoProcessorResult2;

            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "GP Error");
            }
            finally
            {
                System.Text.StringBuilder sb = new System.Text.StringBuilder();
                for (int i = 0; i < gp.MessageCount; i++)
                    sb.AppendLine(gp.GetMessage(i));
                if (sb.Capacity > 0) MessageBox.Show(sb.ToString(), "GP Messages");
            }

        }
        private void featureClassToFeatureClassToolStripMenuItem_Click(object sender, EventArgs e)
        {        
          
            ESRI.ArcGIS.ConversionTools.FeatureClassToFeatureClass featureClassToFeatureClass = new ESRI.ArcGIS.ConversionTools.FeatureClassToFeatureClass();
            featureClassToFeatureClass.in_features = fileGDBPath + "\\" + sourceFCName;
            featureClassToFeatureClass.out_path = sdePath;
            featureClassToFeatureClass.out_name = targetFCName;

            ExecuteGP(featureClassToFeatureClass as IGPProcess);          
           
            
        }

        private void featureClassToGeodatabaseToolStripMenuItem_Click(object sender, EventArgs e)
        {                       
            ESRI.ArcGIS.ConversionTools.FeatureClassToGeodatabase featureClassToGeodatabase = new ESRI.ArcGIS.ConversionTools.FeatureClassToGeodatabase();
            featureClassToGeodatabase.Input_Features = fileGDBPath + "\\" + sourceFCName;
            featureClassToGeodatabase.Output_Geodatabase = sdePath;           

            ExecuteGP(featureClassToGeodatabase as IGPProcess);    
        }

        private void copyFeaturesToolStripMenuItem_Click(object sender, EventArgs e)
        {                  
            ESRI.ArcGIS.DataManagementTools.CopyFeatures copyFeatures = new ESRI.ArcGIS.DataManagementTools.CopyFeatures();
            copyFeatures.in_features = fileGDBPath + "\\" + sourceFCName;
            copyFeatures.out_feature_class = sdePath + "\\" + targetFCName;

            ExecuteGP(copyFeatures as IGPProcess);   
        }

        private void copyToolStripMenuItem_Click(object sender, EventArgs e)
        {                    
            ESRI.ArcGIS.DataManagementTools.Copy copy = new ESRI.ArcGIS.DataManagementTools.Copy();
            copy.in_data = fileGDBPath + "\\" + sourceFCName;
            copy.out_data = sdePath + "\\" + targetFCName;

            ExecuteGP(copy as IGPProcess);   
        }
    }
}

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章