基於ArcEngine顯示網絡圖的流向功能並不難,難的是快速顯示。這節博客的內容在目前幾乎所有的ArcEngine二次開發書籍中都未曾提到過,但卻是一個真正成熟的商業軟件開發所必需具備的。我說的快速,是比常規方法提高93.33倍的效率。
這是我的第一個有關ArcEngine開發的博客,並不是我放棄QGis了,而是現有工作環境中,還沒有辦法完全擁抱QGis,尤其在需要協同開發的任務上。
最近接到一個代碼優化的任務,代碼是基於ArcEngine使用C#二次開發的,其中有一項很主要的功能是顯示網絡圖的流向,就像摘要裏面顯示的圖一樣。
隨便上網一搜,這方面的資源也還不少,其中大家比較推崇的是一本牟乃夏老師的《ArcGIS Engine地理信息系統開發教程–基於C#.NET》書。今天我要講的方法,在效率上比書裏的代碼實例要提高93.33倍左右(實測:同一臺機器,同一個數據庫,運行書裏的代碼大約需要2分20秒,本文方法只需要1.5秒)。當然這個數字並不準確,但是卻說明了效率的顯著提升,並且隨着數據量的增大,效率提升越明顯。實際上,本文的方法幾乎與ArcMap中的相關功能效率一致。同時,要說明的是,牟乃夏老師的那本書很不錯,值得初學者借鑑,裏面很多代碼可以直接使用。
定義
顯示網絡流向標識,首先要做的就是建立一個網絡拓撲結構,這個我就不展開了,假定大家都已經正確建立了一個網絡數據集。然後,就是遍歷網絡中的每一個線要素,判斷其流向。最後,再給它賦上正確的標識符就好了。過程其實蠻簡單的,關鍵是如何優化效率的問題。
普遍的方法
先借用牟乃夏老師書裏的方法來具體說明一下實現。他的方法是
- 遍歷網絡中所有線要素
- 判斷當前線要素的流向
- 根據流向類型,在屏幕適當位置畫上不同的標識符。其中,畫箭頭標識符時還要計算線段起點與終端之間的角度。
下面是整體過程的代碼,必要的地方我在註釋裏面說明。
/// <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;
}
存在的問題及優化思路
使用了以上的代碼後,會發現每次顯示網絡流向,速度非常慢。
根據我的分析,這整個過程中有四個地方存在效率問題:
- 要素遍歷
- 要素遍歷時,使用當前 feature 獲取對應 EID 時調用的 netElements.GetEID 方法
- 每次遍歷出來的要素都要計算一次起點和終點之間的角度
- 畫標識符時,採用添加 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 的渲染速度一樣了呢?
謝謝閱讀。
我還有圖沒有貼,下次補上。