New Network SLPK Generation
Prerequisites
- EPANET model should already have been imported to PSQL with the .NET Network Manager project.
- Access to ArcGIS Pro is required
Prepare Data
- If not already in WGS84 (EPSG:4326), convert the database coordinates using the
NetworkManager/python/convert_db_coordinates.pyscript - Export the 3 tables to CSV
psql -U postgres -d iitd_8_11_2025 -c "\COPY (SELECT * FROM \"Nodes\") TO 'nodes.csv' WITH CSV HEADER"psql -U postgres -d iitd_8_11_2025 -c "\COPY (SELECT * FROM \"Links\") TO 'links.csv' WITH CSV HEADER"psql -U postgres -d iitd_8_11_2025 -c "\COPY (SELECT * FROM \"LinkGeoms\") TO 'linkGeoms.csv' WITH CSV HEADER"
- Configure and run
NetworkManager/python/link_geoms_2d_to_3d_with_attrs.py- NOTE: Make note of the
RuntimeWarning: Normalized/laundered field namewarnings to rename the fields later. - Normally these fields get truncated: XCoordCenter, YCoordCenter, Discriminator, InitSetting
- NOTE: Make note of the
- Open or create an ArcGIS Pro project
- Create a new "Local Scene"
- Catalog > Folders > Add Folder Connection to the shp and csv files.
- Import the "3D_Links" Shapefile that was created by the py script
- (Right click the layer > Data Engineering > Fields) to rename concatenated field names mentioned in the earlier
RuntimeWarning: Normalized/laundered field namewarnings.
- (Right click the layer > Data Engineering > Fields) to rename concatenated field names mentioned in the earlier
- Import the Nodes CSV
Build 3D Nodes
- Right-click the Nodes.csv Standalone Table and select "Create Points from Table > XY Table to Point"
- Check all parameters.
- Define the Z Field as Elevation
- Double check you have the correct Coordinate System selected
- The remaining defaults are OK (at least they were for me)
- Click OK to generate the new layer.
- Open the Geoprocessing pane and search for to select "Split By Attributes"
- Input Table or Features > Select the XY Table to Point layer
- Target Workspace > Press the browse folder icon and select the Default.gdb for this project
- Split Fields > Select Discriminator
- Click Run
- Close the GeoProcessing pane and open the Catalog pane.
- Refresh folders and open the project > Default.gdb to find the resulting layers.
- Add each of the new layers (Junction, Reservoir, Tank) to the map.
Format Nodes
For each type of node, do the following:
- In the contents pane, double click the Junctions layer to open its properties.
- Navigate to the Elevation page and make sure the "Features Are" dropdown has "On the ground" selected.
- Navigate to the Display tab and choose to "Display symbols in scene" with "Real-world units" radio selected.
Define Junction Symbology
- In the contents pane, right click the Junctions layer and select Symbology
- Centered Sphere
- Change the size to a field expression equal to half a meter "0.5".
Define Tank Symbology
- In the contents pane, right click the Junctions layer and select Symbology
- Set it first as a Standing Cylinder
- Customize Symbology:
- Assign a file to the 3D marker instead of cylinder
- For elevated tanks/water towers (minLevel > 0):
C:\jacob\OHRD\DWD\3d-models\water_tank\collada\water_tank.dae - For standpipe tanks (minLevel = 0):
C:\jacob\OHRD\DWD\3d-models\water_tank_standpipe\Water tank.fbx
- For elevated tanks/water towers (minLevel > 0):
- Assign a file to the 3D marker instead of cylinder
- On the size settings, disable "Maintain aspect ratio". Also, omit the
* 0.3048conversion if the units are already in meters.- Define height as the following expression: "$feature.MaxLevel * 0.3048"
- Define width as the following expression: "0.3048 * $feature.Diameter"
Define Reservoir Symbology
- In the contents pane, right click the Junctions layer and select Symbology
- Standing Hexagon
- Set height to constant 1 and width to a constant 75
Build 3D Links
Rename Truncated Property Names
- Right click the 3D_Links layer and select "Data Engineering"
- Click on the "Fields" button next to the Attribute Table button.
- Double click the "Alias" field for each property that needs to be renamed.
- Discrimina -> Discriminator
- InitSettin -> InitSetting
- Save changes and close the Data Engineering tab.
Split Links by Attribute
- Open the Geoprocessing pane and search for to select "Split By Attributes"
- Input Table or Features > Select the 3D_Links layer
- Target Workspace > Press the browse folder icon and select the Default.gdb for this project
- Split Fields > Select Discriminator
- Click Run
- Close the GeoProcessing pane and open the Catalog pane.
- Refresh folders and open the project > Default.gdb to find the resulting layers.
- Add each of the new layers (Pipe, Pump, Valve) to the map.
Prepare Links Properties
Perform these steps for each Links layer...
- Look at the links in the default view. If they are close to the ground, then the default Z values are OK. If they are far too high in the air, then they might need to be converted to meters. Make note of which case this is.
- Double click the 3D_Links layer to open properties.
- Go to the Elevation tab.
- If step 1 indicated Feet or Meters, set the "Vertical units" field accordingly.
- Click Apply.
- Navigate to the Display page and change the Display symbols in scene radio button to "Real-world units"
- Click OK
Define Links Symbology
Perform these steps for each Links layer...
- Right-click the links layer and select Symbology
- For the symbol, select ArcGIS 3D > Tube
- On this page, go to properties to change the color if you'd like.
- Go to Size and define with the arcade expression: $feature.Diameter (Converted to/from feet if necessary
* 0.3048 / 12) - For simplicity, name each of these layers as
{discriminator}Tube, i.e. PipeTube, ValveTube, and PumpTube
Create 3D Markers for Valves and Pumps
Adapt the following script (which can be found in
NetworkManager/python/arcpy/calc_bearing.py). Make sure to update the line_layer and output_points names accordingly.import arcpy
import math
line_layer = "Pump"
output_points = "PumpMarker"
# Get the workspace from the current project
aprx = arcpy.mp.ArcGISProject("CURRENT")
gdb_path = aprx.defaultGeodatabase
output_path = f"{gdb_path}\\{output_points}"
print(f"Creating feature class at: {output_path}")
# Add BEARING field to original line layer first
try:
arcpy.management.AddField(line_layer, "BEARING", "DOUBLE")
print("Added BEARING field to original line layer")
except:
print("BEARING field already exists in line layer or couldn't be added")
# Calculate bearing for original line layer
print("Calculating bearing values for original line layer...")
with arcpy.da.UpdateCursor(line_layer, ["SHAPE@", "BEARING"]) as cursor:
for row in cursor:
line = row[0]
start = line.firstPoint
end = line.lastPoint
delta_x = end.X - start.X
delta_y = end.Y - start.Y
bearing = math.atan2(delta_y, delta_x) * 180 / math.pi
if bearing < 0:
bearing += 360
bearing = 360 - bearing # Correct the bearing direction
row[1] = bearing
cursor.updateRow(row)
# Get spatial reference from input layer
spatial_ref = arcpy.Describe(line_layer).spatialReference
# Get all field names and types from source layer (now including BEARING)
source_fields = []
field_names = []
for field in arcpy.ListFields(line_layer):
if field.type not in ['OID', 'Geometry'] and field.name.upper() not in ['SHAPE', 'OBJECTID']:
source_fields.append(field)
field_names.append(field.name)
# Create output point feature class
arcpy.management.CreateFeatureclass(
gdb_path,
output_points,
"POINT",
spatial_reference=spatial_ref,
has_z="ENABLED"
)
# Add all fields from source layer (including BEARING)
for field in source_fields:
arcpy.management.AddField(
output_path,
field.name,
field.type,
field.precision,
field.scale,
field.length
)
# Prepare field list for cursors
cursor_fields = ["SHAPE@"] + field_names
# Process each line - now BEARING is already calculated and will be copied
with arcpy.da.SearchCursor(line_layer, cursor_fields) as search_cursor:
with arcpy.da.InsertCursor(output_path, cursor_fields) as insert_cursor:
for row in search_cursor:
line = row[0]
# Get centroid
centroid = line.centroid
# Create new row with centroid + all original attributes (including BEARING)
new_row = [centroid] + list(row[1:])
insert_cursor.insertRow(new_row)
print("Feature class created successfully!")
print(f"Location: {output_path}")
print(f"Both layers now have identical schemas with {len(source_fields)} fields including BEARING")
print("\nNext steps you need to manually:")
print("1. Add the layer to your map from the Catalog pane (if it isn't added automatically)")
print("2. Set symbology to 3D Sphere (volumetric)")
print("3. Set elevation mode to 'On the ground'")
print("4. Set to use 'Real world units'")
print("5. Apply rotation using the BEARING field")Temporarily set the symbology to 3D Sphere (volumetric)
Set the elevation mode to 'On the ground'
Set to use 'Real world units'
Apply rotation using the new BEARING field and specify Geographic and NOT Arithmetic
Customize Symbology to assign a file to the 3D marker instead of sphere
Since Valves and Pumps don't have defined sizes, play with the size field until the renderings look good
Give the Valve and Pump tubes a fixed size of 0.1 meters.
Convert Geometries to Multipatch
Semi-Automated Approach
Adapt the following script (which can be found in
NetworkManager/python/arcpy/to_multipatch.py). Make sure to update the in_layers array and out_feature_class path accordingly.in_layers = [
'PumpMarker',
'PumpTube',
'ValveTube',
'ValveMarker',
'PipeTube',
'Tank',
'Reservoir',
'Junction',
]
for layer in in_layers:
arcpy.ddd.Layer3DToFeatureClass(
in_feature_layer=layer,
out_feature_class=f"C:\\jacob\\OHRD\\DWD\\DNV\\dnv_arcgispro_9-26-2025\\MyProject3.gdb\\{layer}_Multipatch",
group_field=None,
disable_materials="DISABLE_COLORS_AND_TEXTURES"
)In ArcGIS Pro, open the View tab and click on Python Window
Paste the modified script into the Python Window input bar and press enter a few times to submit
Manual Approach
These steps must be completed for each of the 6 layers.
- Open the Geoprocessing pane and run the Layer 3D To Feature Class tool.
- Select the input layer.
- Click Run.
- You can then change the Input layer for subsequent layers, but you will need to modify the Output Feature Class to clearly denote which entity type this layer represents, i.e. change from Pipe to Pump, etc.
- Repeat for each of the 6 layers.
Export Layers to SLPK
Semi-Automated Approach
Adapt the following script (which can be found in
NetworkManager/python/arcpy/to_slpk.py). Make sure to update the in_layers array, date_str, and out_slpk path accordingly.in_layers = [
'PumpMarker',
'PumpTube',
'ValveMarker',
'ValveTube',
'PipeTube',
'Tank',
'Reservoir',
'Junction',
]
date_str = "09262025"
for layer in in_layers:
arcpy.management.Create3DObjectSceneLayerPackage(
in_dataset=f'{layer}_Multipatch',
out_slpk=f"C:\\jacob\\OHRD\\DWD\\DNV\\SLPK\\DNV_{layer}_{date_str}.slpk",
out_coor_system='GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],VERTCS["EGM96_height",VDATUM["EGM96_Geoid"],PARAMETER["Vertical_Shift",0.0],PARAMETER["Direction",1.0],UNIT["Meter",1.0]];-400 -400 1000000000;-100000 10000;-100000 10000;8.98315284119521E-09;0.001;0.001;IsHighPrecision',
transform_method=None,
texture_optimization="NONE",
target_cloud_connection=None
)In ArcGIS Pro, open the View tab and click on Python Window
Paste the modified script into the Python Window input bar and press enter a few times to submit
Manual Approach
These steps must be completed for each of the 6 layers.
- Open the Geoprocessing pane and run the Create 3D Object Scene Layer Content tool
- Select the input Multipatch layer
- Use the folder browse to define where to write the SLPK file, i.e.: C:\jacob\OHRD\DWD\MTW\Published_SLPK\MTW_Tanks_05292025.slpk
- Make sure the Output CRS and Geographic Transformation fields are defined properly.
- For LWC, I had to convert from a KY state plane system to WGS 84 using method "NAD_1983_To_WGS_1984_5"
- For MTW, I don't know if we need to convert since it's already in WGS84...
- I set Texture Optimization to Mobile for MTW and None for LWC. I'm not sure what the practical differences are...
- Click Run.
- You can then change the Input layer for subsequent layers, but you will need to modify the Ouput file name.
- Repeat for each of the 6 multipatch layers.
NOTE: This process takes a while for each layer. Large layers, such as LWC Pipes, can take more than 20 minutes.