Skip to main content

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

  1. If not already in WGS84 (EPSG:4326), convert the database coordinates using the NetworkManager/python/convert_db_coordinates.py script
  2. 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"
  3. Configure and run NetworkManager/python/link_geoms_2d_to_3d_with_attrs.py
    • NOTE: Make note of the RuntimeWarning: Normalized/laundered field name warnings to rename the fields later.
    • Normally these fields get truncated: XCoordCenter, YCoordCenter, Discriminator, InitSetting
  4. Open or create an ArcGIS Pro project
  5. Create a new "Local Scene"
  6. Catalog > Folders > Add Folder Connection to the shp and csv files.
  7. 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 name warnings.
  8. Import the Nodes CSV

Build 3D Nodes

  1. Right-click the Nodes.csv Standalone Table and select "Create Points from Table > XY Table to Point"
  2. 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)
  3. Click OK to generate the new layer.
  4. 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
  5. Close the GeoProcessing pane and open the Catalog pane.
  6. Refresh folders and open the project > Default.gdb to find the resulting layers.
  7. Add each of the new layers (Junction, Reservoir, Tank) to the map.

Format Nodes

For each type of node, do the following:

  1. In the contents pane, double click the Junctions layer to open its properties.
  2. Navigate to the Elevation page and make sure the "Features Are" dropdown has "On the ground" selected.
  3. Navigate to the Display tab and choose to "Display symbols in scene" with "Real-world units" radio selected.

Define Junction Symbology

  1. In the contents pane, right click the Junctions layer and select Symbology
  2. Centered Sphere
  3. Change the size to a field expression equal to half a meter "0.5".

Define Tank Symbology

  1. In the contents pane, right click the Junctions layer and select Symbology
  2. Set it first as a Standing Cylinder
  3. 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
  4. On the size settings, disable "Maintain aspect ratio". Also, omit the * 0.3048 conversion 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

  1. In the contents pane, right click the Junctions layer and select Symbology
  2. Standing Hexagon
  3. Set height to constant 1 and width to a constant 75

Rename Truncated Property Names

  1. Right click the 3D_Links layer and select "Data Engineering"
  2. Click on the "Fields" button next to the Attribute Table button.
  3. Double click the "Alias" field for each property that needs to be renamed.
    • Discrimina -> Discriminator
    • InitSettin -> InitSetting
  4. Save changes and close the Data Engineering tab.
  1. 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
  2. Close the GeoProcessing pane and open the Catalog pane.
  3. Refresh folders and open the project > Default.gdb to find the resulting layers.
  4. Add each of the new layers (Pipe, Pump, Valve) to the map.

Perform these steps for each Links layer...

  1. 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.
  2. Double click the 3D_Links layer to open properties.
  3. Go to the Elevation tab.
  4. If step 1 indicated Feet or Meters, set the "Vertical units" field accordingly.
  5. Click Apply.
  6. Navigate to the Display page and change the Display symbols in scene radio button to "Real-world units"
  7. Click OK

Perform these steps for each Links layer...

  1. Right-click the links layer and select Symbology
  2. For the symbol, select ArcGIS 3D > Tube
    • On this page, go to properties to change the color if you'd like.
  3. Go to Size and define with the arcade expression: $feature.Diameter (Converted to/from feet if necessary * 0.3048 / 12)
  4. For simplicity, name each of these layers as {discriminator}Tube, i.e. PipeTube, ValveTube, and PumpTube

Create 3D Markers for Valves and Pumps

  1. 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")
  2. Temporarily set the symbology to 3D Sphere (volumetric)

  3. Set the elevation mode to 'On the ground'

  4. Set to use 'Real world units'

  5. Apply rotation using the new BEARING field and specify Geographic and NOT Arithmetic

  6. Customize Symbology to assign a file to the 3D marker instead of sphere

  7. Since Valves and Pumps don't have defined sizes, play with the size field until the renderings look good

  8. Give the Valve and Pump tubes a fixed size of 0.1 meters.

Convert Geometries to Multipatch

Semi-Automated Approach

  1. 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"
    )
  2. In ArcGIS Pro, open the View tab and click on Python Window

  3. 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.

  1. Open the Geoprocessing pane and run the Layer 3D To Feature Class tool.
  2. Select the input layer.
  3. Click Run.
  4. 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.
  5. Repeat for each of the 6 layers.

Export Layers to SLPK

Semi-Automated Approach

  1. 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
    )
  2. In ArcGIS Pro, open the View tab and click on Python Window

  3. 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.

  1. Open the Geoprocessing pane and run the Create 3D Object Scene Layer Content tool
  2. Select the input Multipatch layer
  3. 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
  4. 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...
  5. I set Texture Optimization to Mobile for MTW and None for LWC. I'm not sure what the practical differences are...
  6. Click Run.
  7. You can then change the Input layer for subsequent layers, but you will need to modify the Ouput file name.
  8. 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.