Kevin JarrettによるUnsplashの写真
2025年3月17日更新:python用のサンプルJSONパーサーがこの記事に添付されています。また、ArcGIS Pro SDKを使用して構築されたサンプルパーサーは2025年ArcGIS Developer & Technology SummitのGitHubリポジトリにあります。
ArcGIS Utility Networkの接続モデルを使って独自の分析を構築したいと思ったことはありませんか?しかし、Export SubnetworkやTraceツールが生成するJSONファイルの解析に苦労したことはありませんか?あるいは、ユーティリティネットワークと統合したいネットワーク分析製品をお持ちかもしれません。
Pythonは、JSONを使いやすいグラフに変換する強力なパーサーを構築する際の切り札となります。この文章ではその方法を示します。グラフが手元にあれば、自分自身でネットワーク分析を行ったり、他の任意の分析ツールに利用したりする自由が得られます。
この記事は、昨年6月に公開されたJourney to the Utility Network: Network Integrationsの記事を補完することを目的としています。この記事の例を最大限に活用するためには、まずその記事を読み、Pythonの基本的な知識とArcGIS Utility Networkの基本概念および用語に慣れておくことをお勧めします。もし助けが必要なら、ArcGIS Pro Python ReferenceやUtility Network Vocabularyページが良い出発点です。
また、この記事に添付されているサンプルJSONとパーサーも必ずダウンロードして、一緒に進めてください。この記事のJSONはWater Utility Networkモデルからのものですが、添付された例ファイルは異なるデータモデルやリリースにも適応可能です。この記事は添付スクリプトの構造に沿って書かれていますが、以下のリストでファイル内のどのJSON要素がこの記事で扱われているか確認できます。
- Source Mapping
- Spatial Reference
- Features
- Connectivity
- Associations
この例で使用するJSONファイルにはExport Subnetworkからの3つの主要な結果タイプが含まれています:features, connectivity, and associationsです。理解しやすく解析しやすくするために、このエクスポートにはジオメトリとドメイン記述も含めましたが、これらのオプションは結果として得られるJSONファイルのサイズを大幅に増加させるため、アプリケーションでこれらの情報が不要な場合はチェックを外すべきです。
ファイルの読み込み
ファイルを解析する最初のステップは、ディスクからJSONファイルをメモリに読み込むことです。ファイル内容を直接メモリに読み込むこともできますが、UJSONなどのJSON解析ライブラリを使って読み込み・解析する方が良いです。これにより、ファイル内のJSONを通常のPythonデータ構造として扱うことができ、中括弧や引用符の解析方法を気にせずに済みます。この処理はParseJsonExportメソッドの冒頭で確認できます。
with open(json_path, "r") as json_file:
json_content = ujson.load(json_file)
del json_file
ファイルを読み込みUJSONライブラリで解析した後、その内容は他のPythonオブジェクトと同様に操作できます。JSONファイル内のほとんどの要素は辞書または配列で表現されています。以下にJSONで表現されたファイル構造の概要を示します:
{
"connectivity": [
{ 36 },
],
"featureElements": [
{ 39 },
],
"associations": [
{ 42 },
],
"sourceMapping": { 43 },
"spatialReference": { 44 }
}
出力内容を増減させる方法について詳しくは上記参照の Utility Network Integrations article を参照してください。ここでは解析スクリプトが各要素をどのように利用しているか見ていきましょう。
Source Mapping(ソースマッピング)
'sourceMapping'要素は辞書であり、ユーティリティネットワーク内すべてのネットワークソース層名をユーティリティネットワークが管理する内部識別子(network source id)へ変換します。これは重要です。なぜならJSONファイル内では機器や線路、構造物など特徴はnetwork source idで参照されているためです。エクスポート時に説明文を含めない場合、この変換辞書を見る必要があります。
"sourceMapping": {
"1": "UN_134_Associations",
"2": "UN_134_SystemJunctions",
"3": "",
"4": "StructureJunction",
"6": "StructureBoundary",
"7": "StructureJunctionObject",
"5": "StructureLine",
"8": "StructureEdgeObject",
"9": "WaterDevice",
"11": "WaterAssembly",
"12": "WaterJunction",
"14": "WaterJunctionObject",
"10": "WaterLine",
"13": "WaterSubnetLine",
"15": "WaterEdgeObject"
},
'sourceMapping'要素は辞書なので、その力を活用することは簡単です。一度この要素への参照があれば、必要な時いつでもnetwork source IDを翻訳できます。この例で含まれるエクスポートにはすべてのネットワークソース説明文がありますが、説明文なしエクスポートの場合でもパーサーはsource mappingsを使って情報翻訳しています。
feature_values['networkSourceName'] = source_mapping.get(element['networkSourceId'], "Unknown")
'sourceMapping'による処理方法をご覧いただいたので、次は'spatialReference'要素がジオメトリ作成支援にどう役立つか見てみましょう。
'Spatial Reference'(空間参照)
'spatialReference'要素はエクスポート元ユーティリティネットワークの空間参照情報を示し、このファイル内ジオメトリ利用時のみ考慮します。最初に行うべきことは、このspatial reference JSON要素からArcPyで空間参照オブジェクトへ変換することです。
spatial_reference_element = json_content.get("spatialReference", None)
if spatial_reference_element is not None:
spatial_reference = arcpy.SpatialReference(spatial_reference_element["wkid"])
結果を可視化したい場合、ジオメトリやデータセット作成時または別座標系への変換時には空間参照情報が必須です。以下例をご覧ください:
arcpy.CreateFeatureclass_management(output_gdb,"trace_point","POINT",spatial_reference=spatial_reference)
arcpy.CreateFeatureclass_management(output_gdb,"trace_line","POINT",spatial_reference=spatial_reference)
なお、ご利用中バージョンやエクスポート方法によっては結果JSONファイルに'spatialReference'要素がない場合があります。その場合は以下コードでユーティリティネットワークデータセット自体から空間参照取得可能です:
un_description = arcpy.Describe(utility_network)
spatial_reference = un_description.spatialReference
これらフィーチャクラスへデータ格納するにはJSONファイル内'features'要素からジオメトリと属性情報抽出が必要ですが、それこそ次にスクリプトが行う処理です。
'Features'(フィーチャ)
'featureElements'要素にはサブネットワークまたはトレース内すべてフィーチャ属性情報があります。この部分解釈最大の課題はネットワーク上で表現されるフィーチャが地図上表示より細分化されている点です。例で説明しましょう。
地図上ではジャンクションや単端子機器はポイントフィーチャとして表現されます。一方JSONでは単一要素として対応しています。
二端子以上ある機器を見ると興味深くなります。地図上では単一フィーチャですがエクスポートでは複数要素として表現されます。それぞれ端子と端子経路ごと別々フィーチャ扱いです。
一見不思議ですが、この重要性はこのファイル内容と接続性部分(個々端子やエッジ参照)比較すると理解できます。一意識別にはnetwork source id, object id, terminal id使えます(接続性一致)が、本パーサーでは冗長性削減しnetwork source idとobject idだけで一意識別しています。このため属性情報検索にはnetwork source idとobject idだけ必要です。
feature_key = f"{element['networkSourceId']}${element['objectId']}"
次に線フィーチャ表現方法も異なります。ユーティリティネットワークでは各線フィーチャ複数エッジ要素で表現されます。中間接続ある線の場合複数エッジ要素で線区間ごと表現します。この例ではジオデータベース内線フィーチャが2つファイル内フィーチャ要素持ち、それぞれ異なる区間表しています。
各フィーチャ属性一意識別にはnetwork source idとobject id十分ですが、ジオメトリ統合には各エッジfrom positionとto position考慮必要です。それぞれ全体線形状部分集合だからです。この例では最初区間59%、次区間41%占めています。
ポイントフィーチャ同様、本パーサーでは全エッジ属性統合し単一フィーチャ化しています。ただしジオメトリについてはJSON要素from positionと線座標保存し複数フィーチャから単一ジオメトリ統合可能としています。
以下のスニペットでは、ポイントおよびラインフィーチャのジオメトリの格納方法の違いを見ることができます。
for key, value in element.items():
if key == "geometry":
geometry_element = value
if "x" in geometry_element:
# ポイントを作成する
geometries[feature_key] = arcpy.Point(geometry_element["x"],geometry_element["y"],geometry_element["z"],geometry_element["m"])
elif "fromPosition" in element:
# ラインセグメントのジオメトリとパーセントに沿った位置を追加する
# これを使って後で全てのラインセグメントをつなぎ合わせます
other_geometries = geometries.get(feature_key, [])
other_geometries.append([element["fromPosition"], geometry_element])
geometries[feature_key] = other_geometries
この記事ではジオメトリの作成については触れていませんが、添付ツールのCreateGeometriesメソッドを見ることで、このエクスポートを使ってフィーチャクラスにジオメトリをどのように入力するかの例を見ることができます。もしエクスポートでジオメトリ情報を使用しない場合は、出力からジオメトリを除外し、パーサーで無視することでパーサーを簡素化し、ファイルサイズを大幅に削減できます。
接続性
JSONファイル内の「connectivity」要素は、ノードとエッジの集合である無向グラフです。各接続要素は2つのノードを接続するエッジを記述しており、JSON要素のfrom/to属性はそれぞれノードを指し、via属性はエッジを参照します。
要素間の接続性
添付スクリプトはProcessConnectivityメソッドを使ってこれを実現する方法を示しています。このメソッドは接続要素をfrom/via/toの各コンポーネントに分割し、2つの異なるアプローチで解析します。
- fromおよびto要素は、それぞれネットワークソースID、オブジェクトID、および端末IDによって一意に識別されます。
- via要素はネットワークソースID、オブジェクトID、「from」位置、および「to」位置によって一意に識別されます。
for element in connectivity_element:
from_key = f"{element['fromNetworkSourceId']}${element['fromObjectId']}${element['fromTerminalId']}"
to_key = f"{element['toNetworkSourceId']}${element['toObjectId']}${element['toTerminalId']}"
via_key = f"{element['viaNetworkSourceId']}${element['viaObjectId']}${element['viaPositionFrom']}${element['viaPositionTo']}"
すべてのfrom/via/to要素が解析された後、この情報を使って隣接リストを作成できます。このパーサーの場合、各from/via/to要素を独自のオブジェクトとして扱い、それら間の接続を保存します。この構造はネットワーク探索アルゴリズムやnetworkxなどのPythonライブラリによる解析に適しています。networkx.
# 接続性を保存する
from_connections = connectivity.get(from_key, [])
from_connections.append(via_key)
via_connections = connectivity.get(via_key, [])
via_connections.append(to_key)
# via接続が接続関連の場合、両側にエッジを追加する必要があります
# 関連にデジタイズ方向指定が許可されていないためです
if element["viaNetworkSourceId"] == 1:
to_connections = connectivity.get(to_key, [])
to_connections.append(via_key)
connectivity[to_key] = to_connections
via_connections.append(from_key)
connectivity[from_key] = from_connections
connectivity[via_key] = via_connections
このJSONファイル内の接続性には方向性、流れ、または通行可能性に関する情報がないことを覚えておくことが重要です。より平易に言えば、from、via、およびtoという名前から示唆される方向性はユーティリティネットワーク内の実際の流れを表すものではなく、ライン上の頂点順序や関連付けの起点/終点(どちらも流れを示すものではありません)を表しています。ユーティリティネットワークの真の流れはトレース実行時にソースまたはシンクから計算され、現在JSONには出力されていません。下図では一部エッジが「反転」しているように見えますが、この領域では実際には右側の高圧部から左側の低圧部へ水が流れているにもかかわらず、地図とJSON内のラインのデジタイズ方向は左から右への「from」端となっています。
接続性要素にはfeatures要素と同様にフィーチャ用ジオメトリが含まれており、それぞれ空間的なfrom/via/to要素にはそのジオメトリが含まれています。また、エッジフィーチャが複数のエッジ要素を含む場合、そのエッジ要素は全体ジオメトリの部分集合に対応します。以下をご覧ください。
フィーチャと接続性の処理方法について説明したので、最後に残るピースは「associations」要素です。
{
"viaNetworkSourceId": 10,
"viaGlobalId": "{29C0B239-9AC0-4E76-9E5A-A912D46CF267}",
"viaObjectId": 22258,
"viaPositionFrom": 0.59016310696272389,
"viaPositionTo": 1,
"viaGeometry": {
"hasZ": true,
"hasM": true,
"paths": [
[
[
1032056.63583742827,
1857139.22013278306,
0.00010000000474974513,
null
],
[
1032024.145088762,
1857269.34913761169,
0.00010000000474974513,
null
]
]
]
},
"fromNetworkSourceId": 12,
"fromGlobalId": "{6F988B32-CA53-4DE2-AA77-A79544386D3F}",
"fromObjectId": 7271,
"fromTerminalId": 1,
"fromGeometry": {
"x": 1032056.63583742827,
"y": 1857139.22013278306,
"z": 0.00010000000474974513,
"m": null
},
"toNetworkSourceId": 12,
"toGlobalId": "{4F36DEA2-656D-468D-B1F9-30849B04FC20}",
"toObjectId": 7723,
"toTerminalId": 1,
"toGeometry": {
"x": 1032024.145088762,
"y": 1857269.34913761169,
"z": 0.00010000000474974513,
"m": null
}
},
Associations
The “associations” JSON element is very similar to the “connectivity” element JSON in structure with the exception that each element consists of only a “from” and “to” element. Additionally, the utility network models associations at the feature level, so when cross-referencing associations to the features element you only need to consider the network source id and the global id of the “from” and “to” feature.
{
"associationType": "containment",
"fromNetworkSourceId": 6,
"fromGlobalId": "{E4693E2A-441D-404F-871F-4D4C5E6AE20A}",
"fromTerminalId": 1,
"toNetworkSourceId": 9,
"toGlobalId": "{FC1540AA-5965-4BDD-B207-F6AD0B40B5BE}",
"toTerminalId": 1,
"fromNetworkSourceName": "StructureBoundary",
"fromTerminalName": "Single Terminal",
"toNetworkSourceName": "WaterDevice",
"toTerminalName": "Single Terminal"
},
By parsing the JSON this way, you can filter out what appear as duplicate associations. Doing this before outputting your results will make it easier to interpret your results as well as reduce the size of any files you output.
{
"associationType": "containment",
"fromNetworkSourceId": 5,
"fromGlobalId": "{8F686F75-3E07-4EFA-BAA4-EAE03F40D93F}",
"fromTerminalId": -1,
"toNetworkSourceId": 10,
"toGlobalId": "{92D69B81-FF17-4C5A-85CB-0733AFE8F02A}",
"toTerminalId": -1,
"fromNetworkSourceName": "StructureLine",
"fromTerminalName": "",
"toNetworkSourceName": "WaterLine",
"toTerminalName": ""
},
… association repeats multiple times …
{
"associationType": "containment",
"fromNetworkSourceId": 5,
"fromGlobalId": "{8F686F75-3E07-4EFA-BAA4-EAE03F40D93F}",
"fromTerminalId": -1,
"toNetworkSourceId": 10,
"toGlobalId": "{92D69B81-FF17-4C5A-85CB-0733AFE8F02A}",
"toTerminalId": -1,
"fromNetworkSourceName": "StructureLine",
"fromTerminalName": "",
"toNetworkSourceName": "WaterLine",
"toTerminalName": ""
},
Note that because we are referencing associations by network source id and global id, while referencing other features by their network source id and object id we will need to do some translation when comparing the different datasets. In this example, the use of the object id was a deliberate choice to make it simpler to select and interact with the geodatabase, but for other applications it may be easier to not use object id for any comparisons and just use the global id.
Conclusion
Now that you have read this article you are equipped with a better understanding of interpreting and parsing the features, connectivity, and associations elements in JSON files produced by the utility network. To inspire you further, attached to this article is a sample Python tool that demonstrates how these techniques can be used to craft your own custom analysis. Happy parsing!