ArcEngine -- 快速顯示網絡流向標識

基於ArcEngine顯示網絡圖的流向功能並不難,難的是快速顯示。這節博客的內容在目前幾乎所有的ArcEngine二次開發書籍中都未曾提到過,但卻是一個真正成熟的商業軟件開發所必需具備的。我說的快速,是比常規方法提高93.33倍的效率。

這裏寫圖片描述

這是我的第一個有關ArcEngine開發的博客,並不是我放棄QGis了,而是現有工作環境中,還沒有辦法完全擁抱QGis,尤其在需要協同開發的任務上。

最近接到一個代碼優化的任務,代碼是基於ArcEngine使用C#二次開發的,其中有一項很主要的功能是顯示網絡圖的流向,就像摘要裏面顯示的圖一樣。

隨便上網一搜,這方面的資源也還不少,其中大家比較推崇的是一本牟乃夏老師的《ArcGIS Engine地理信息系統開發教程–基於C#.NET》書。今天我要講的方法,在效率上比書裏的代碼實例要提高93.33倍左右(實測:同一臺機器,同一個數據庫,運行書裏的代碼大約需要2分20秒,本文方法只需要1.5秒)。當然這個數字並不準確,但是卻說明了效率的顯著提升,並且隨着數據量的增大,效率提升越明顯。實際上,本文的方法幾乎與ArcMap中的相關功能效率一致。同時,要說明的是,牟乃夏老師的那本書很不錯,值得初學者借鑑,裏面很多代碼可以直接使用。

定義

顯示網絡流向標識,首先要做的就是建立一個網絡拓撲結構,這個我就不展開了,假定大家都已經正確建立了一個網絡數據集。然後,就是遍歷網絡中的每一個線要素,判斷其流向。最後,再給它賦上正確的標識符就好了。過程其實蠻簡單的,關鍵是如何優化效率的問題。

普遍的方法

先借用牟乃夏老師書裏的方法來具體說明一下實現。他的方法是

  1. 遍歷網絡中所有線要素
  2. 判斷當前線要素的流向
  3. 根據流向類型,在屏幕適當位置畫上不同的標識符。其中,畫箭頭標識符時還要計算線段起點與終端之間的角度。

下面是整體過程的代碼,必要的地方我在註釋裏面說明。

/// <summary>
/// 顯示流向
/// </summary>
private void showDir()
{
   try
   {
       // 得到當前幾何網絡所在的FeatureDataset
       IFeatureLayer featureLayer = m_layer as IFeatureLayer;
       IFeatureDataset featureDataset = featureLayer.FeatureClass.FeatureDataset;

       // 得到接口轉換獲得當前所加載的幾何網絡
       INetworkCollection2 networkCollection2 = featureDataset as INetworkCollection2;
       IGeometricNetwork geometricNetwork = networkCollection2.get_GeometricNetwork(0);

       // 獲取當前的邏輯網絡
       INetwork network = geometricNetwork.Network;
       IUtilityNetworkGEN utilityNetworkGEN = network as IUtilityNetworkGEN;

       // 這個是關鍵方法
       ShowFlowForFeatureClass(featureLayer.FeatureClass, utilityNetworkGEN);
       // 下面的這個MapControl改成你代碼中的地圖控件變量名稱
       m_app.MapControl.ActiveView.PartialRefresh(esriViewDrawPhase.esriViewGraphics, null, m_app.MapControl.ActiveView.FullExtent);
       m_app.MapControl.Refresh();
   }
   catch 
   { 
   System.Windows.Forms.MessageBox.Show("渲染出錯"); 
   return; 
   }
}

然後是 ShowFlowForFeatureClass 這個方法:

/// <summary>
/// 使用繪製的方式顯示流向,速度非常慢
/// </summary>
/// <param name="featureClass">所需繪製的圖層</param>
/// <param name="utilityNetworkGEN">當前幾何網絡對應的邏輯網絡</param>
private void ShowFlowForFeatureClass(IFeatureClass featureClass, IUtilityNetworkGEN utilityNetworkGEN)
{
    // 使用INetElements接口查詢網絡要素的ElementID
    INetElements netElements = utilityNetworkGEN as INetElements;
    // 定義相關變量
    esriFlowDirection flowDirection = new esriFlowDirection();
    int currentEID = -1;

    // 對整個圖層進行遍歷,從而對每個邊要素進行繪製
    IFeatureCursor featureCursor = featureClass.Search(null, false);
    int featureClassID = featureClass.FeatureClassID;

    IFeature feature = featureCursor.NextFeature();
    while (feature != null)
    {
        currentEID = netElements.GetEID(featureClassID, feature.OID, 0, esriElementType.esriETEdge);

        // 使用IUtilityNetworkGEN接口查詢每個網絡邊要素的流向
        flowDirection = utilityNetworkGEN.GetFlowDirection(currentEID);

        // 進行繪製
        DrawArrowElementForEdgeElement(feature, m_app.MapControl.Map, flowDirection);
        feature = featureCursor.NextFeature();
    }
}

最後是 DrawArrowElementForEdgeElement 方法,以及它使用到的 GetLineAngleFrom2Points 方法:

/// <summary>
/// 對地圖中的要素根據網絡流向的值進行繪製。
/// </summary>
/// <param name="feature">要繪製的要素</param>
/// <param name="map">當前地圖</param>
/// <param name="flowDirection">網絡流向值</param>
private void DrawArrowElementForEdgeElement(IFeature feature, IMap map, esriFlowDirection flowDirection)
{
    // 得到管線的中心點,作爲繪製箭頭符號的位置
    IPolyline polyline = feature.Shape as IPolyline;
    IPoint middlePoint = new PointClass();
    polyline.QueryPoint(esriSegmentExtension.esriNoExtension, polyline.Length / 2, false, middlePoint);
    // 定義相關變量
    IArrowMarkerSymbol arrowMarkerSymbol = new ArrowMarkerSymbolClass();
    ISimpleMarkerSymbol simpleMarkerSymbol = new SimpleMarkerSymbolClass();
    IElement element = null;

    arrowMarkerSymbol.Color = CommonFunctions.GetColorByRGBValue(0, 0, 0);
    arrowMarkerSymbol.Size = 12;
    element = new MarkerElementClass();
    element.Geometry = middlePoint;
    ((IMarkerElement)element).Symbol = arrowMarkerSymbol;
    // 將Element的名字設置爲Flow,便於今後清除
    ((IElementProperties)element).Name = "Flow";

    // 如果流向沿管線數字化方向
    if (flowDirection == esriFlowDirection.esriFDWithFlow)
    {
        // 通過線段的起點和終點得到流向的角度
        arrowMarkerSymbol.Angle = GetLineAngleFrom2Points(polyline.FromPoint, polyline.ToPoint);

    }
    // 如果流向與數字化方向相反
    else if (flowDirection == esriFlowDirection.esriFDAgainstFlow)
    {
        arrowMarkerSymbol.Angle = GetLineAngleFrom2Points(polyline.ToPoint, polyline.FromPoint);
        arrowMarkerSymbol.Color = CommonFunctions.GetColorByRGBValue(0, 0, 0);
        arrowMarkerSymbol.Size = 12;
        element = new MarkerElementClass();
        element.Geometry = middlePoint;
        ((IMarkerElement)element).Symbol = arrowMarkerSymbol;
        ((IElementProperties)element).Name = "Flow";
    }
    // 如果是未確定的流向
    else if (flowDirection == esriFlowDirection.esriFDIndeterminate)
    {
        simpleMarkerSymbol.Color = CommonFunctions.GetColorByRGBValue(0, 0, 0);
        simpleMarkerSymbol.Size = 8;
        element = new MarkerElementClass();
        element.Geometry = middlePoint;
        ((IMarkerElement)element).Symbol = simpleMarkerSymbol;
        ((IElementProperties)element).Name = "Flow";
    }
    // 如果流向尚未初始化
    else
    {
        simpleMarkerSymbol.Color = CommonFunctions.GetColorByRGBValue(255, 0, 0);
        simpleMarkerSymbol.Size = 8;
        element = new MarkerElementClass();
        element.Geometry = middlePoint;
        ((IMarkerElement)element).Symbol = simpleMarkerSymbol;
        ((IElementProperties)element).Name = "Flow";
    }
    // 添加箭頭符號
    IGraphicsContainer gc = map as IGraphicsContainer;
    gc.AddElement(element, 0);
}

/// <summary>
/// 通過線段的起點和終點得到線段的角度
/// </summary>
/// <param name="startPoint">線段的起點</param>
/// <param name="endPoint">線段的終點</param>
/// <returns>線段的角度</returns>
private double GetLineAngleFrom2Points(IPoint startPoint, IPoint endPoint)
{
    double angle = -1;
    //如果起點和終點的縱座標相等
    if (startPoint.Y == endPoint.Y)
    {
        if (startPoint.X < endPoint.X)
            angle = 0;
        else
            angle = 180;
    }
    //如果起點和終點的橫座標相等
    else if (startPoint.X == endPoint.X)
    {
        if (startPoint.Y < endPoint.Y)
            angle = 90;
        else
            angle = 270;
    }
    //如果起點的縱座標小於終點的縱座標
    else if (startPoint.Y < endPoint.Y)
    {
        if (startPoint.X < endPoint.X)
            angle = (Math.Atan((endPoint.Y - startPoint.Y) / (endPoint.X - startPoint.X))) * 180 / Math.PI;
        else
            angle = Math.Atan((startPoint.X - endPoint.X) / (endPoint.Y - startPoint.Y)) * 180 / Math.PI + 90;
    }
    //如果起點的縱座標大於終點的縱座標
    else
    {
        if (endPoint.X < startPoint.X)
            angle = Math.Atan((startPoint.Y - endPoint.Y) / (startPoint.X - endPoint.X)) * 180 / Math.PI + 180;
        else
            angle = 360 - Math.Atan((startPoint.Y - endPoint.Y) / (endPoint.X - startPoint.X)) * 180 / Math.PI;
    }
    return angle;
}

存在的問題及優化思路

使用了以上的代碼後,會發現每次顯示網絡流向,速度非常慢。

根據我的分析,這整個過程中有四個地方存在效率問題:

  1. 要素遍歷
  2. 要素遍歷時,使用當前 feature 獲取對應 EID 時調用的 netElements.GetEID 方法
  3. 每次遍歷出來的要素都要計算一次起點和終點之間的角度
  4. 畫標識符時,採用添加 element 的方式繪製在地圖上

依次分析這四個問題。第一個要素遍歷的問題,是沒有辦法避免的(但是有沒有辦法讓它更快呢?後面再說)。第二個問題是採用這種 feature 的方式獲取 EID 的常規手段,主要是爲了下一步得到流向:

flowDirection = utilityNetworkGEN.GetFlowDirection(currentEID);

實際上,可以發現,feature 在這裏的作用僅僅是爲了得到 EID,有沒有辦法直接遍歷 EID 呢?答案是可以的。看下面的代碼:

IEIDHelper eidHelper = new EIDHelper();
eidHelper.GeometricNetwork = geometricNetwork;
eidHelper.ReturnFeatures = true;
eidHelper.ReturnGeometries = true;

IEnumEIDInfo enumEidInfo = eidHelper.CreateEnumEIDInfo(enumEID);
IEIDInfo eidInfo = enumEidInfo.Next();

通過這個 IEIDHelper 接口,可以達到直接遍歷 EID 的目的。

實際上,這樣遍歷有一個好處,就是網絡類型提供了 CreateNetBrowser 的方法來達到快速遍歷要素的目的,建立索引這種機制我想就不用多解釋了吧。具體是下面這個代碼:

utilityNetworkGEN.CreateNetBrowser(esriElementType.esriETEdge);

這樣,再返回來看第一個問題,既然都要遍歷 feature,爲何不用 EID 來得到 feature 呢?這樣做會加快不少運行速度。具體實現看後文的代碼。

我們繼續看第三個問題,這個問題在操作上其實與 ArcMap 是不相同的。使用這種計算角度的方式放置箭頭符號,會使得箭頭指向與線段走向之間有偏移,雖然可以說明不是直線的情況下線段的流向,但是個人覺得有點反常規思維。這個可以直接不計算了,提高一些運行速度,雖然並沒有提高太多。

第四個問題是基本上所有需要畫標識的時候,最容易想到的方法,使用添加 element 的方式,想要在哪裏畫,想畫多少個都沒有問題。但是這裏有個不好的地方,如果你把這個線圖層關掉,會發現地圖上“遊蕩”着數不清的箭頭標識,感覺很奇怪。

仔細研究一下 ArcMap 的做法。當把線圖層關掉後,會發現流向標識也不見了。再看看它的設置裏面,如下圖:

這裏寫圖片描述

原來,它不是畫的 element,而是改變了當前線段的渲染樣式。要做到這一點,可以參照圖層樣式設置裏面的唯一值分類渲染:

這裏寫圖片描述

這樣就有思路了,在代碼中,也應該是使用改變圖層樣式的方式更合適,實際上,這也會加快運行的速度。

優化後的代碼

/// <summary>
/// 顯示流向
/// </summary>
private void showDir()
{
    try
    {
        // 得到當前幾何網絡所在的FeatureDataset
        IFeatureLayer featureLayer = m_layer as IFeatureLayer;
        IFeatureDataset featureDataset = featureLayer.FeatureClass.FeatureDataset;

        // 得到接口轉換獲得當前所加載的幾何網絡
        INetworkCollection2 networkCollection2 = featureDataset as INetworkCollection2;
        IGeometricNetwork geometricNetwork = networkCollection2.get_GeometricNetwork(0);

        // 獲取當前的邏輯網絡
        INetwork network = geometricNetwork.Network;
        IUtilityNetworkGEN utilityNetworkGEN = network as IUtilityNetworkGEN;

        // 保存原始渲染樣式
        m_lineFeauteRenderer = (featureLayer as IGeoFeatureLayer).Renderer;
        IGeoFeatureLayer geofl = featureLayer as IGeoFeatureLayer;
        IFeatureRenderer feaRenderer = geofl.Renderer;
        if (feaRenderer is ISimpleRenderer)
        {
            ISimpleRenderer sr = feaRenderer as ISimpleRenderer;
            m_lineSymbol = sr.Symbol as ILineSymbol;
        }

        // 這個方法改變了,注意!!!
        showFlowForFeatureLayer(featureLayer, utilityNetworkGEN);
        m_app.MapControl.ActiveView.PartialRefresh(esriViewDrawPhase.esriViewGraphics, null, 
            m_app.MapControl.ActiveView.FullExtent);
        m_app.MapControl.Refresh();
    }
    catch { System.Windows.Forms.MessageBox.Show("渲染出錯"); return; }
}

然後是 showFlowForFeatureLayer 這個方法,這裏面實際上包含了 IUniqueValueRenderer 的使用,由於不是本文的重點,就只展示代碼就好了:

/// <summary>
/// 優化後遍歷方式之後,使用渲染樣式的方式顯示流向,速度非常快
/// </summary>
/// <param name="featureLayer"></param>
/// <param name="utilityNetworkGEN"></param>
private void showFlowForFeatureLayer(IFeatureLayer featureLayer, IUtilityNetworkGEN utilityNetworkGEN)
{
    // 使用INetElements接口查詢網絡要素的ElementID
    INetElements netElements = utilityNetworkGEN as INetElements;
    IEnumNetEID enumEID = utilityNetworkGEN.CreateNetBrowser(esriElementType.esriETEdge);

    // 得到接口轉換獲得當前所加載的幾何網絡
    INetworkCollection2 networkCollection2 = featureLayer.FeatureClass.FeatureDataset as INetworkCollection2;
    IGeometricNetwork geometricNetwork = networkCollection2.get_GeometricNetwork(0);

    IEIDHelper eidHelper = new EIDHelper();
    eidHelper.GeometricNetwork = geometricNetwork;
    eidHelper.ReturnFeatures = true;
    eidHelper.ReturnGeometries = true;

    IEnumEIDInfo enumEidInfo = eidHelper.CreateEnumEIDInfo(enumEID);
    IEIDInfo eidInfo = enumEidInfo.Next();

    IUniqueValueRenderer uniqueValueRenderer = new UniqueValueRenderer();
    uniqueValueRenderer.FieldCount = 1;
    string filedName = featureLayer.FeatureClass.OIDFieldName;
    uniqueValueRenderer.set_Field(0, filedName);

    // 初始化Symbol,這裏面有一個 CommonFunctions 類我這裏沒有提供,該類的 GetColorByRGBValue 方法實際上就是由 RGB 的值來構造一個 IColor 對象並返回,非常簡單,讀者自行實現即可。
    ISymbol flowSymbol = CreateCartographicLineSymbol(new ArrowMarkerSymbol() as IMarkerSymbol, 
        CommonFunctions.GetColorByRGBValue(0,90,255), 16);
    IArrowMarkerSymbol arrowMarkerSymbol = new ArrowMarkerSymbol();
    arrowMarkerSymbol.Angle = 180;
    ISymbol againstFlowSymbol = CreateCartographicLineSymbol(arrowMarkerSymbol as IMarkerSymbol, 
        CommonFunctions.GetColorByRGBValue(0,90,255), 16);
    ISymbol indeterminateSymbol = CreateCartographicLineSymbol(new SimpleMarkerSymbol() as IMarkerSymbol, 
        CommonFunctions.GetColorByRGBValue(0,0,0), 10);
    ISymbol nonInitSymbol = CreateCartographicLineSymbol(new SimpleMarkerSymbol() as IMarkerSymbol, 
        CommonFunctions.GetColorByRGBValue(255, 90, 0), 10);

    uniqueValueRenderer.AddValue("with flow", filedName, flowSymbol);
    uniqueValueRenderer.AddValue("against flow", filedName, againstFlowSymbol);
    uniqueValueRenderer.AddValue("inderterminate", filedName, indeterminateSymbol);
    uniqueValueRenderer.AddValue("non-init", filedName, nonInitSymbol);

    IFeature feature = null;
    int currentEID = -1;
    esriFlowDirection flowDirection = new esriFlowDirection();

    while (eidInfo != null)
    {
        currentEID = eidInfo.EID;
        feature = eidInfo.Feature;
        // 使用IUtilityNetworkGEN接口查詢每個網絡邊要素的流向
        flowDirection = utilityNetworkGEN.GetFlowDirection(currentEID);
        // 如果流向沿管線數字化方向
        if (flowDirection == esriFlowDirection.esriFDWithFlow)
        {
            uniqueValueRenderer.AddReferenceValue(feature.OID.ToString(), "with flow");
        }
        // 如果流向與數字化方向相反
        else if (flowDirection == esriFlowDirection.esriFDAgainstFlow)
        {
            uniqueValueRenderer.AddReferenceValue(feature.OID.ToString(), "against flow");
        }
        // 如果是未確定的流向
        else if (flowDirection == esriFlowDirection.esriFDIndeterminate)
        {
            uniqueValueRenderer.AddReferenceValue(feature.OID.ToString(), "inderterminate");
        }
        // 如果流向尚未初始化
        else
        {
            uniqueValueRenderer.AddReferenceValue(feature.OID.ToString(), "non-init");
        }

        eidInfo = enumEidInfo.Next();
    }

    IGeoFeatureLayer geoFeatureLayer = featureLayer as IGeoFeatureLayer;
    geoFeatureLayer.Renderer = uniqueValueRenderer as IFeatureRenderer;
}

/// <summary>
/// 創建由點元素修飾的線樣式
/// </summary>
/// <param name="markerSymbol"></param>
/// <param name="color"></param>
/// <param name="size"></param>
/// <returns></returns>
private ISymbol CreateCartographicLineSymbol(IMarkerSymbol markerSymbol, IColor color, int size)
{
    ICartographicLineSymbol cartographicLineSymbol = new CartographicLineSymbol();
    cartographicLineSymbol.Color = m_lineSymbol.Color;
    cartographicLineSymbol.Width = m_lineSymbol.Width;

    setCartoLineSymbol(cartographicLineSymbol, m_lineSymbol);
    ILineProperties pLineProp = cartographicLineSymbol as ILineProperties;
    pLineProp.DecorationOnTop = true;

    ISimpleLineDecorationElement pSimpleLineDecoElem = new SimpleLineDecorationElementClass();
    pSimpleLineDecoElem.AddPosition(0.5);
    markerSymbol.Size = size;
    markerSymbol.Color = color;
    pSimpleLineDecoElem.MarkerSymbol = markerSymbol as IMarkerSymbol;

    ILineDecoration pLineDecoration = new LineDecorationClass();
    pLineDecoration.AddElement(pSimpleLineDecoElem as ILineDecorationElement);
    pLineProp.LineDecoration = pLineDecoration;

    ILineSymbol pLineSymbol = cartographicLineSymbol as ILineSymbol;

    return pLineSymbol as ISymbol;
}

OK,運行一下優化後的代碼,是不是幾乎與 ArcMap 的渲染速度一樣了呢?

謝謝閱讀。

我還有圖沒有貼,下次補上。

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