Foto von Kevin Jarrett auf Unsplash
Update 17.03.2025: Ein Beispiel-JSON-Parser für Python ist diesem Artikel beigefügt. Sie finden auch einen Beispielparser, der mit dem ArcGIS Pro SDK erstellt wurde, im GitHub-Repo vom ArcGIS Developer & Technology Summit 2025.
Haben Sie schon einmal Ihre eigene Analyse mit dem Konnektivitätsmodell des ArcGIS Utility Network erstellen wollen, hatten aber Schwierigkeiten, die JSON-Dateien zu parsen, die das Export Subnetwork- oder Trace-Werkzeug erzeugt? Vielleicht haben Sie ein Netzwerk-Analyseprodukt, das Sie mit dem Utility Network integrieren möchten?
Python kann Ihr Ass im Ärmel sein, wenn Sie einen leistungsstarken Parser erstellen, der Ihr JSON in einen nutzbaren Graphen umwandelt, und dieser Artikel zeigt Ihnen, wie das geht. Mit dem Graphen in der Hand haben Sie die Freiheit, Ihre eigene Netzwerkanalyse durchzuführen oder ihn zu verwenden, um ein anderes Analysewerkzeug Ihrer Wahl zu füllen.
Dieser Artikel soll den Journey to the Utility Network: Network Integrations-Artikel ergänzen, der letzten Juni veröffentlicht wurde. Um das Beste aus den Beispielen in diesem Artikel herauszuholen, empfehle ich Ihnen, diesen Artikel zuerst zu lesen und eine grundlegende Vertrautheit mit Python sowie den Grundkonzepten und der Terminologie des ArcGIS Utility Network zu haben. Die ArcGIS Pro Python Reference und die Seite Utility Network Vocabulary sind gute Ressourcen zum Einstieg, falls Sie Hilfe benötigen.
Laden Sie unbedingt auch das Beispiel-JSON und den Parser herunter, die diesem Artikel beigefügt sind, damit Sie mitverfolgen können. Das JSON in diesem Artikel stammt aus dem Water Utility Network-Modell, aber Sie werden feststellen, dass die beigefügten Beispieldateien an verschiedene Datenmodelle oder Versionen angepasst werden können. Der Artikel ist so geschrieben, dass er der Struktur des beigefügten Skripts folgt, aber Sie können die untenstehende Liste verwenden, um zu sehen, welche JSON-Elemente in der Datei in diesem Artikel behandelt werden.
- Source Mapping
- Spatial Reference
- Features
- Connectivity
- Associations
Die JSON-Datei, die wir in diesem Beispiel verwenden werden, enthält die drei Haupt-Ergebnistypen von Export Subnetwork: Features, Connectivity und Associations. Ich habe mich entschieden, Geometrien und Domänenbeschreibungen in diesen Export einzuschließen, um die Datei leichter verständlich und parsbar zu machen. Diese Optionen erhöhen jedoch deutlich die Größe der resultierenden JSON-Datei. Wenn Ihre Anwendung diese Informationen nicht benötigt, sollten Sie diese Optionen deaktiviert lassen.
Datei laden
Das Erste, was wir tun müssen, um die Datei zu parsen, ist das Laden der JSON-Datei von der Festplatte in den Speicher. Während wir den Inhalt der Datei direkt in den Speicher lesen könnten, ist es am besten, eine JSON-Parsing-Bibliothek wie UJSON zu verwenden, um die Datei für uns zu lesen und zu parsen. Dadurch können wir mit dem JSON in der Datei unter Verwendung normaler Python-Datenstrukturen interagieren, ohne uns Gedanken darüber machen zu müssen, wie man all die geschweiften Klammern und Anführungszeichen in der Datei parst. Dies sehen Sie am Anfang der Methode ParseJsonExport.
with open(json_path, "r") as json_file:
json_content = ujson.load(json_file)
del json_file
Sobald wir die Datei geladen und die UJSON-Bibliothek sie geparst hat, können wir mit ihrem Inhalt wie mit jedem anderen Python-Objekt interagieren. Die meisten Elemente in der JSON-Datei sind entweder als Dictionaries oder Arrays dargestellt. Eine Übersicht über die Dateistruktur im JSON-Format sehen Sie unten:
{
"connectivity": [
{ 026; },
],
"featureElements": [
{ 026; },
],
"associations": [
{ 026; },
],
"sourceMapping": { 026; },
"spatialReference": { 026; }
}
Für weitere Informationen darüber, wie man die Ausgabe der Datei steuert, um mehr oder weniger Informationen einzuschließen, verweisen wir auf den oben genannten Utility Network Integrations article referenced above. For now, let’s continue analyzing how the parsing script makes use of each of these elements.
Source Mapping
The “sourceMapping” element provides a dictionary that translates the layer names for all the network sources in your utility network to an internal identifier (network source id) maintained by the utility network. This is important because the rest of the JSON file references features by their network source id, so if you want to know whether a feature is a device, line, or structure and you chose not to include descriptions in your export you will need to refer to this translation.
"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"
},
As the source mapping element is a dictionary, harnessing its power is simple. Once we have a reference to this element, we can easily utilize it to translate network source IDs whenever the need arises. The export included in this example already includes descriptions for all our network sources, however, you can see that the parser is still using the source mappings to translate information in cases where you create an export that doesn’t include domain descriptions.
feature_values['networkSourceName'] = source_mapping.get(element['networkSourceId'], "Unknown")
Now that you’ve seen how we use source mappings to process elements, let’s look at how we can use the spatial reference element to help create a geometry.
Spatial Reference
The “spatialReference” element describes the spatial reference of the utility network from which the export was taken and only needs to be considered when making use of the geometries included in the file. The first step we need to take is to turn the spatial reference element in JSON into a spatial reference object using ArcPy.
spatial_reference_element = json_content.get("spatialReference", None)
if spatial_reference_element is not None:
spatial_reference = arcpy.SpatialReference(spatial_reference_element["wkid"])
If you want to visualize your results the spatial reference must be included when creating any geometries or datasets or when translating the geometries to another coordinate system. You can see an example of this below:
arcpy.CreateFeatureclass_management(output_gdb,"trace_point","POINT",spatial_reference=spatial_reference)
arcpy.CreateFeatureclass_management(output_gdb,"trace_line","POINT",spatial_reference=spatial_reference)
Note, depending on your release and which method you use to export your JSON the resulting JSON file may not have a spatial reference element. If this is the case, you can always use the spatial reference of the utility network dataset itself using the following code:
un_description = arcpy.Describe(utility_network)
spatial_reference = un_description.spatialReference
Populating these feature classes requires us to parse the features element of the JSON file to extract geometry and attribute information. Conveniently enough, that’s the next thing that our script does.
Features
The “featuresElements” element in the JSON file contains the attribute information for all the features included in your subnetwork or trace. The biggest challenge when interpreting this section of the file is to remember that the network’s representation of your features is more granular than the representation you see in the map. To illustrate, let’s look at an example.
In your map, a junction or single-terminal device is represented as a point feature. In the JSON file, you will find that it corresponds to a single element.
Things get more interesting when you look at a device that has two (or more) terminals. In the map this will appear as a single feature, but in the export, it is represented as multiple elements. The export treats each terminal and terminal path as a separate feature.
Dies mag auf den ersten Blick etwas ungewöhnlich erscheinen, aber seine Bedeutung wird klarer, wenn man beginnt, den Inhalt dieser Datei mit den Connectivity-Teilen der JSON-Datei zu vergleichen, welche die einzelnen Terminals und Kanten referenzieren, um Konnektivität herzustellen. Um jedes Punktelement in der JSON-Datei eindeutig zu identifizieren, könnten wir die network source id, object id und terminal id des Features verwenden (was mit der Connectivity übereinstimmen würde). Allerdings haben wir uns in unserem Parser entschieden, jedes Feature nur anhand der network source id und object id eindeutig zu identifizieren. Dies reduziert Redundanz und bedeutet, dass man zur Abfrage von Attributinformationen nur eine network source id und object id benötigt.
feature_key = f"{element['networkSourceId']}${element['objectId']}"
Der nächste Unterschied liegt darin, wie Linienfeatures dargestellt werden. Das liegt daran, dass im Utility Network jedes Linienfeature durch ein oder mehrere Edge-Elemente repräsentiert wird. Wenn eine Linie eine Midspan-Konnektivität hat, verwendet das Netzwerk mehrere Edge-Elemente zur Darstellung jeder Abschnittsabschnitts der Linie. Dies sehen Sie im folgenden Beispiel: Das Linienfeature in unserer Geodatabase hat zwei Feature-Elemente in der Datei, von denen jedes einen anderen Segmentabschnitt darstellt.
Um die Attribute jedes Features eindeutig zu identifizieren reicht es aus, network source id und object id des Features zu betrachten. Um jedoch die Geometrien für das Feature zusammenzuführen müssen wir die from position und to position jeder Kante berücksichtigen da jedes JSON-Element einen Teilbereich der Gesamtgeometrie der Linie darstellt. Am Beispiel oben sieht man dass das erste Segment 59 % der Länge des Shapes ausmacht während das zweite Segment 41 % ausmacht.
Ähnlich wie bei Punktfeatures hat unser Parser beschlossen alle Attribute für Edge-Elemente in einem einzigen Feature zusammenzufassen unter Verwendung von network source id und object id. Für die Geometrien speichern wir jedoch zusätzlich die „from position“ des JSON-Elements zusammen mit den Koordinaten der Linie ab damit wir Geometrien mehrerer Features zu einer einzigen Geometrie konsolidieren können.
Sie können den Unterschied sehen, wie wir Geometrien für Punkt- und Linienmerkmale im folgenden Ausschnitt speichern.
für Schlüssel, Wert in element.items():
wenn Schlüssel == "geometry":
geometry_element = Wert
wenn "x" in geometry_element:
# Erstelle einen Punkt
geometries[feature_key] = arcpy.Point(geometry_element["x"],geometry_element["y"],geometry_element["z"],geometry_element["m"])
elif "fromPosition" in element:
# Füge die Geometrie des Liniensegments zusammen mit der Position als Prozentwert hinzu
# Wir verwenden dies, um später alle Liniensegmente zusammenzufügen
other_geometries = geometries.get(feature_key, [])
other_geometries.append([element["fromPosition"], geometry_element])
geometries[feature_key] = other_geometries
Obwohl wir in diesem Artikel nicht über das Erstellen von Geometrien sprechen, können Sie sich die CreateGeometries-Methode im angehängten Tool ansehen, um ein Beispiel dafür zu erhalten, wie man eine Feature-Class mit den Geometrien aus diesem Export füllt. Wenn Ihr Export keine Geometrieinformationen benötigt, können Sie Ihren Parser vereinfachen und die Dateigröße erheblich reduzieren, indem Sie Geometrien einfach aus Ihrer Ausgabe ausschließen und Ihren Parser diese ignorieren lassen.
Konnektivität
Das "connectivity"-Element in der JSON-Datei ist ein ungerichteter Graph, der eine Sammlung von Knoten und Kanten darstellt. Jedes Verbindungselement beschreibt eine Kante, die zwei Knoten verbindet, wobei die from/to-Attribute des JSON-Elements jeweils auf einen Knoten verweisen und die via-Attribute des JSON-Elements auf die Kante verweisen.
Konnektivität zwischen Elementen
Das beigefügte Skript zeigt, wie dies mit der Methode ProcessConnectivity erreicht werden kann. Diese Methode analysiert die Konnektivitätselemente in ihre separaten from/via/to-Komponenten mit zwei verschiedenen Ansätzen.
- Die from- und to-Elemente werden jeweils eindeutig durch ihre Netzwerkquellen-ID, Objekt-ID und Terminal-ID identifiziert.
- Die via-Elemente werden dann eindeutig durch die Netzwerkquellen-ID, Objekt-ID, "from"-Position und "to"-Position identifiziert.
für 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']}"
Sobald alle from/via/to-Elemente geparst wurden, können wir diese Informationen verwenden, um eine Adjazenzliste zu erstellen. Im Fall dieses Parsers behandeln wir jedes from/via/to-Element als eigenes Objekt und speichern die Verbindungen zwischen ihnen. Diese Struktur eignet sich gut für Analysen mit einem Netzwerkdurchlaufalgorithmus oder für Analysen mit Python-Bibliotheken wie networkx.
# Speichere die Konnektivität
from_connections = connectivity.get(from_key, [])
from_connections.append(via_key)
via_connections = connectivity.get(via_key, [])
via_connections.append(to_key)
# Wenn die via-Verbindung eine Konnektivitätsassoziation ist, müssen wir Kanten zu beiden Seiten hinzufügen
# Da wir keine digitalisierte Richtung bei einer Assoziation zulassen
wenn 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
Es ist wichtig zu beachten, dass die Konnektivität in dieser JSON-Datei keine Richtung, keinen Fluss oder Informationen über Durchquerbarkeit besitzt. Einfacher ausgedrückt bedeutet die durch die Benennung von from, via und to implizierte Richtung nicht den tatsächlichen Fluss innerhalb des Versorgungsnetzes; sie repräsentiert stattdessen die Reihenfolge der Scheitelpunkte einer Linie oder den Ursprung/Ziel der Assoziation (keines davon weist auf Fluss hin). Der tatsächliche Fluss des Versorgungsnetzes wird bei der Durchführung einer Spurung unter Verwendung von Quellen oder Senken berechnet und wird derzeit nicht im JSON ausgegeben. Ein Beispiel hierfür sehen Sie in der Grafik unten, wo einige der Kanten scheinbar „umgedreht“ sind. In diesem Bereich fließt das Wasser tatsächlich von einem Hochdruckgebiet rechts zu einem Niederdruckgebiet links; jedoch zeigt die digitalisierte Richtung der Linien in der Karte und im JSON das „from“-Ende der Linien von links nach rechts an.
Das Konnektivitätselement enthält Geometrien für Features ähnlich dem Features-Element, was bedeutet, dass jedes räumliche from/via/to-Element seine Geometrie enthält und das Geometry Edge Element einem Teil der Gesamtgeometrie der Kante entsprechen kann, wenn das Kantenelement mehrere Edge-Elemente enthält, wie unten gezeigt.
Nachdem wir nun behandelt haben, wie man Features und Konnektivität verarbeitet, ist das letzte fehlende Puzzlestück das "associations"-Element.
{
"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!