photo par Kevin Jarrett sur Unsplash
Mise à jour 17/03/2025 : Un exemple d'analyseur JSON pour python est joint à cet article. Vous pouvez également trouver un exemple d'analyseur construit avec le ArcGIS Pro SDK dans le dépôt GitHub du Sommet des développeurs et technologies ArcGIS 2025.
Avez-vous déjà voulu construire votre propre analyse en utilisant le modèle de connectivité du ArcGIS Utility Network, mais avez eu du mal à analyser les fichiers JSON que l'outil Export Subnetwork ou Trace produit ? Peut-être avez-vous un produit d'analyse réseau que vous souhaitez intégrer au utility network ?
Python peut être votre atout majeur pour construire un analyseur puissant qui transforme votre JSON en un graphe utilisable, et cet article vous montrera comment. Avec ce graphe en main, vous aurez la liberté de réaliser votre propre analyse réseau ou de l'utiliser pour alimenter un autre outil d'analyse de votre choix.
Cet article est destiné à compléter l'article Journey to the Utility Network: Network Integrations publié en juin dernier. Pour tirer le meilleur parti des exemples dans cet article, je recommande de lire d'abord cet article et d'avoir une familiarité de base avec Python ainsi qu'avec les concepts et la terminologie de base du ArcGIS Utility Network. La référence ArcGIS Pro Python Reference et la page Utility Network Vocabulary sont de bonnes ressources pour commencer si vous avez besoin d'aide.
Assurez-vous également de télécharger le JSON d'exemple et l'analyseur joints à cet article afin de pouvoir suivre. Le JSON dans cet article provient du modèle Water Utility Network, mais vous constaterez que les fichiers d'exemple joints peuvent être adaptés à différents modèles de données ou versions. L'article est écrit pour suivre la structure du script joint, mais vous pouvez utiliser la liste ci-dessous pour voir quels éléments JSON dans le fichier sont abordés dans cet article.
- Cartographie des sources
- Référence spatiale
- Entités
- Connectivité
- Associations
Le fichier JSON que nous utiliserons dans cet exemple inclut les trois principaux types de résultats de Export Subnetwork: entités, connectivité et associations. J'ai choisi d'inclure les géométries et descriptions de domaine dans cette exportation pour rendre le fichier plus facile à comprendre et à analyser, mais ces options augmenteront sensiblement la taille du fichier JSON résultant, donc si votre application n'a pas besoin de ces informations, vous devriez laisser ces options décochées.
Chargement du fichier
La première chose que nous devons faire pour analyser le fichier est de charger le fichier JSON depuis le disque en mémoire. Bien que nous puissions lire directement le contenu du fichier en mémoire, il est préférable d'utiliser une bibliothèque d'analyse JSON comme UJSON pour lire et analyser le fichier pour nous. Cela nous permettra d'interagir avec le JSON dans le fichier en utilisant des structures de données Python normales sans avoir à se soucier de comment analyser toutes les accolades et guillemets dans le fichier. Vous pouvez voir cela au début de la méthode ParseJsonExport.
avec open(json_path, "r") as json_file:
json_content = ujson.load(json_file)
del json_file
Une fois que nous avons chargé le fichier et permis à la bibliothèque UJSON de l'analyser, nous pouvons alors interagir avec son contenu comme nous le ferions avec n'importe quel autre objet Python. La plupart des éléments dans le fichier JSON sont soit représentés par des dictionnaires soit par des tableaux. Vous pouvez voir un aperçu de la structure du fichier, telle que représentée en JSON ci-dessous :
{
"connectivity": [
{ 026 },
],
"featureElements": [
{ 026 },
],
"associations": [
{ 026 },
],
"sourceMapping": { 026 },
"spatialReference": { 026 }
}
Pour plus d'informations sur comment contrôler la sortie du fichier pour inclure plus ou moins d'informations, référez-vous à l'article Utility Network Integrations article mentionné ci-dessus. Pour l'instant, continuons à analyser comment le script d'analyse utilise chacun de ces éléments.
Cartographie des sources
L'élément « sourceMapping » fournit un dictionnaire qui traduit les noms des couches pour toutes les sources réseau dans votre utility network vers un identifiant interne (network source id) maintenu par le utility network. Ceci est important car le reste du fichier JSON fait référence aux entités par leur network source id, donc si vous voulez savoir si une entité est un appareil, une ligne ou une structure et que vous avez choisi de ne pas inclure les descriptions dans votre exportation, vous devrez vous référer à cette traduction.
"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"
},
Comme l'élément sourceMapping est un dictionnaire, exploiter sa puissance est simple. Une fois que nous avons une référence à cet élément, nous pouvons facilement l'utiliser pour traduire les IDs des sources réseau chaque fois que cela est nécessaire. L'exportation incluse dans cet exemple comprend déjà des descriptions pour toutes nos sources réseau, cependant, vous pouvez voir que l'analyseur utilise toujours les cartographies des sources pour traduire les informations dans les cas où vous créez une exportation qui n'inclut pas les descriptions de domaine.
feature_values['networkSourceName'] = source_mapping.get(element['networkSourceId'], "Inconnu")
Maintenant que vous avez vu comment nous utilisons les cartographies des sources pour traiter les éléments, regardons comment nous pouvons utiliser l'élément spatial reference pour aider à créer une géométrie.
Référence spatiale
L'élément « spatialReference » décrit la référence spatiale du utility network dont l'exportation a été prise et ne doit être considéré que lors de l'utilisation des géométries incluses dans le fichier. La première étape consiste à transformer l'élément spatial reference en JSON en un objet référence spatiale en utilisant 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)
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.
This may seem a little unusual at first glance, but its importance is understood once you start comparing the contents of this file with the connectivity portions of the JSON file which reference the individual terminal and edges to establish connectivity. To uniquely identify each point element in the JSON file we could use the network source id, object id, and terminal id of the feature (which would match the connectivity). However, you will notice that in our parser we have chosen to uniquely identify each feature using just the network source id and object id of each feature. This reduces redundancy and means that to look up attribute information you need only a network source id and object id.
feature_key = f"{element['networkSourceId']}${element['objectId']}"
The next difference you will note is in how line features are represented. This is because, in the utility network, each line feature is represented by one or more edge elements. When a line has midspan connectivity then the network uses multiple edge elements to represent each of the sections of the line. You can see this in the example below where the line feature in our geodatabase has two feature elements in the file, each representing a different segment of the line.
To uniquely identify the attributes of each feature, it is sufficient to consider the network source id and object id of the feature. However, to consolidate the geometries for the feature we must consider the from position and to position of each edge because each JSON element represents a subset of the overall line’s geometry. Using the example from above we can see that the first segment accounts for 59% of the shape’s length, with the second segment accounting for the remaining 41%.
Similar to how we handled point features, our parser has chosen to consolidate all the attributes for edge elements into a single feature using the network source id and object id. For the geometries however, we are storing the “from position” of the JSON element along with the coordinates of the line so we can consolidate the geometries from multiple features into a single geometry.
Vous pouvez voir la différence entre la façon dont nous stockons les géométries pour les entités de points et de lignes dans l'extrait ci-dessous.
pour clé, valeur dans element.items() :
si clé == "geometry" :
geometry_element = valeur
si "x" dans geometry_element :
# Créer un point
geometries[feature_key] = arcpy.Point(geometry_element["x"],geometry_element["y"],geometry_element["z"],geometry_element["m"])
elif "fromPosition" dans element :
# Ajouter la géométrie du segment de ligne, ainsi que la position en pourcentage
# Nous utilisons cela pour assembler tous les segments de ligne plus tard
other_geometries = geometries.get(feature_key, [])
other_geometries.append([element["fromPosition"], geometry_element])
geometries[feature_key] = other_geometries
Bien que nous ne discutions pas de la création de géométries dans cet article, vous pouvez consulter la méthode CreateGeometries dans l'outil joint pour un exemple de comment remplir une classe d'entités avec les géométries en utilisant cette exportation. Si votre exportation n'a pas besoin d'utiliser les informations de géométrie, vous pouvez simplifier votre analyseur et réduire considérablement la taille de votre fichier en excluant simplement les géométries de votre sortie et en faisant en sorte que votre analyseur les ignore.
Connectivité
L'élément "connectivity" dans le fichier JSON est un graphe non orienté qui est une collection de nœuds et d'arêtes. Chaque élément de connexion décrit une arête qui connecte deux nœuds où les attributs from/to de l'élément JSON se réfèrent chacun à un nœud et les attributs via de l'élément JSON référencent l'arête.
Connectivité entre éléments
Le script inclus montre comment réaliser cela en utilisant la méthode ProcessConnectivity. Cette méthode analyse les éléments de connectivité en leurs composants séparés from/via/to en utilisant deux approches différentes.
- Les éléments from et to sont chacun identifiés de manière unique par leur id source réseau, id objet, et leur id terminal.
- Les éléments via sont ensuite identifiés de manière unique par l'id source réseau, l'id objet, la position "from", et la position "to".
pour element dans 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']}"
Une fois que tous les éléments from/via/to ont été analysés, nous pouvons alors utiliser ces informations pour créer une liste d'adjacence. Dans le cas de cet analyseur, nous traitons chaque élément from/via/to comme son propre objet et stockons les connexions entre chacun d'eux. Cette structure est bien adaptée pour une analyse utilisant un algorithme de parcours de réseau, ou pour une analyse utilisant des bibliothèques Python telles que networkx.
# Stocker la connectivité
from_connections = connectivity.get(from_key, [])
from_connections.append(via_key)
via_connections = connectivity.get(via_key, [])
via_connections.append(to_key)
# Si la connexion via est une association de connectivité, nous devons ajouter des arêtes des deux côtés
# Puisque nous ne permettons pas de spécifier la direction numérisée sur une association
si 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
Il est important de se rappeler que la connectivité dans ce fichier JSON ne possède pas de directionnalité, ni de flux, ni d'information sur la traversabilité. Plus simplement, la directionnalité impliquée par le nommage des informations from, via et to ne représente pas un flux réel au sein du réseau utilitaire, elle représente plutôt l'ordre des sommets dans une ligne ou l'origine/destination de l'association (aucun des deux n'étant indicatif du flux). Le véritable flux du réseau utilitaire est calculé en utilisant des sources ou des puits lorsque le traçage est effectué et n'est pas actuellement exporté dans le JSON. Vous pouvez voir un exemple de cela dans le graphique ci-dessous où certaines arêtes semblent être "inversées". Dans cette zone, le flux réel d'eau s'écoule d'une zone à haute pression à droite vers une zone à basse pression à gauche, cependant, la direction numérisée des lignes sur la carte et dans le JSON affiche l'extrémité "from" des lignes venant de gauche à droite.
L'élément connectivité inclut des géométries pour les entités d'une manière similaire à l'élément features, ce qui signifie que chaque élément spatial from/via/to inclura sa géométrie et que l'élément edge geometry peut correspondre à un sous-ensemble de la géométrie globale pour l'arête si l'entité edge contient plusieurs éléments edge comme vu ci-dessous.
Maintenant que nous avons couvert comment traiter les entités et la connectivité, il reste la dernière pièce du puzzle : l'élément "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!