I'm looking to export the entirety of our enterprise LRS out of ArcMap, but with a caveat. When there is a concurrency, I would like only the dominant route present in the exported data.
I've got a workflow that seems to give me what I want, but it's a lot of steps.
Here's what I'm currently using:
Does anyone have a simpler workflow to accomplish this? Do you see any flaws in this current workflow?
Thanks in advance.
Edit (1/30/2018):
I'm including a Python script that implements something similar to Amit Hazra's model builder screenshot. Maybe someone will find it useful. Thanks for the tips, RHUG.
Edit (3/17/2020)
I ended up turning this into an ArcGIS Desktop Geoprocessing Tool as well. It's only been tested on ArcGIS Desktop 10.5.1 with an LRSN Route Network stored in a file geodatabase or SQL Server 2016 geodatabase. If you're using a different RDBMS, the where_clause that applies the Temporal View Date may be different.
GitHub - vitale232/ExportDominantNetwork: Export dominant routes from an Esri R&H ALRS.
I've also attached a .zip directory containing the tool as of today's date.
import datetime
import os
import arcpy
def make_dominant_network(input_routes, concurrency_table, overwrite_flag=False):
"""
After creating gaps in the input network where concurrencies exist,
the geometries must be split to single parts. Then use the m-values
from the single part geometries to create new routes. This is done to avoid
non-monotonic routes. Non-monotonicity can be introduced from multipart
outputs in GP tools, which are likely to reorder the part indices
"""
dom_table_path = os.path.join(
os.path.dirname(input_routes),
'dominant_table'
)
dom_events_path = os.path.join(
os.path.dirname(input_routes),
'dominant_event'
)
erased_routes_path = os.path.join(
os.path.dirname(input_routes),
'milepoint_concurr_gaps'
)
erased_routes_singlepart_path = os.path.join(
os.path.dirname(input_routes),
'milepoint_concurr_gaps_singlepart'
)
merged_routes_path = os.path.join(
os.path.dirname(input_routes),
'milepoint_dom_event_merge'
)
output_path = os.path.join(
os.path.dirname(input_routes),
'milepoint_dominant_network'
)
if overwrite_flag and arcpy.Exists(dom_table_path):
print(' overwrite deleting {}'.format(dom_table_path))
arcpy.Delete_management(dom_table_path)
if overwrite_flag and arcpy.Exists(dom_events_path):
print(' overwrite deleting {}'.format(dom_events_path))
arcpy.Delete_management(dom_events_path)
if overwrite_flag and arcpy.Exists(erased_routes_path):
print(' overwrite deleting {}'.format(erased_routes_path))
arcpy.Delete_management(erased_routes_path)
if overwrite_flag and arcpy.Exists(merged_routes_path):
print(' overwrite deleting {}'.format(merged_routes_path))
arcpy.Delete_management(merged_routes_path)
if overwrite_flag and arcpy.Exists(output_path):
print(' overwrite deleting {}'.format(output_path))
arcpy.Delete_management(output_path)
if not overwrite_flag and arcpy.Exists(merged_routes_path):
field_names = [field.name for field in arcpy.ListFields(merged_routes_path)]
if all(['m_min' in field_names, 'm_max' in field_names]):
add_m_fields = False
else:
add_m_fields = True
# Begin geoprocessing logic
if not arcpy.Exists(dom_table_path):
print('\nSubsetting concurrency table to only dominant events')
where_clause = "(DominantFlag = 1) AND (DominantError <> 4)"
print(' {}'.format(where_clause))
arcpy.TableToTable_conversion(
concurrency_table,
os.path.dirname(dom_table_path),
os.path.basename(dom_table_path),
where_clause=where_clause
)
if not arcpy.Exists(dom_events_path):
print('\nCreating event for route dominance')
line_props = 'RouteId LINE FromMeasure ToMeasure'
dom_layer = arcpy.MakeRouteEventLayer_lr(
input_routes, 'ROUTE_ID', dom_table_path, line_props,
'dom_layer'
)
arcpy.CopyFeatures_management(
dom_layer, dom_events_path
)
print(' {}'.format(dom_events_path))
if not arcpy.Exists(erased_routes_path):
print('\nCreating network gaps at concurrencies')
arcpy.Erase_analysis(input_routes, dom_events_path, erased_routes_path)
print(' {}'.format(erased_routes_path))
if not arcpy.Exists(erased_routes_singlepart_path):
print('\nSplitting gapped network to single-part geometries')
arcpy.MultipartToSinglepart_management(
erased_routes_path, erased_routes_singlepart_path
)
print(' {}'.format(erased_routes_singlepart_path))
if not arcpy.Exists(merged_routes_path):
print('\nMerging dominant routes with gapped network')
field_mapping = map_fields(
[erased_routes_singlepart_path, 'ROUTE_ID'],
[dom_events_path, 'RouteId'],
'ROUTE_ID'
)
arcpy.Merge_management(
[erased_routes_singlepart_path, dom_events_path],
merged_routes_path,
field_mapping
)
print(' {}'.format(merged_routes_path))
add_m_fields = True
if add_m_fields:
print('\nAdding m-values to the merged routes attribute table')
merged_field_names = [
field.name for field in arcpy.ListFields(merged_routes_path)
]
if not 'm_min' in merged_field_names:
arcpy.AddField_management(
merged_routes_path,
'm_min',
'DOUBLE',
)
print(' created field: m_min')
if not 'm_max' in merged_field_names:
arcpy.AddField_management(
merged_routes_path,
'm_max',
'DOUBLE',
)
print(' created field: m_max')
update_fields = ['SHAPE@', 'm_min', 'm_max']
with arcpy.da.UpdateCursor(merged_routes_path, update_fields) as update_cursor:
for row in update_cursor:
shape = row[0]
m_min = shape.extent.MMin
m_max = shape.extent.MMax
update_cursor.updateRow([shape, m_min, m_max])
print(' table update: complete')
if not arcpy.Exists(output_path):
print('\nCreating final network of only dominant routes')
arcpy.CreateRoutes_lr(
merged_routes_path, 'ROUTE_ID', output_path,
measure_source='TWO_FIELDS',
from_measure_field='m_min', to_measure_field='m_max',
build_index='INDEX'
)
print(' {}'.format(output_path))
def map_fields(table_field_a, table_field_b, output_name):
field_mappings = arcpy.FieldMappings()
field_map = arcpy.FieldMap()
field_map.addInputField(*table_field_a)
field_map.addInputField(*table_field_b)
output_field = field_map.outputField
output_field.name = output_name
field_map.outputField = output_field
field_mappings.addFieldMap(field_map)
return field_mappings
def main():
input_routes = r'D:\Pavement\Input_Data\Milepoint.gdb\Milepoint_20190116'
concurrency_table = r'D:\Pavement\Input_Data\Milepoint.gdb\route_concurrencies_tvd20190116'
overwrite_flag = False
make_dominant_network(input_routes, concurrency_table, overwrite_flag=overwrite_flag)
if __name__ == '__main__':
start_time = datetime.datetime.now()
print('Running script: {0}\nStart time: {1}'.format(
os.path.abspath(__file__), start_time)
)
main()
end_time = datetime.datetime.now()
print('\nCompleted at: {}.\nTime to complete: {}'.format(
end_time, end_time - start_time)
)
Solved! Go to Solution.
Hello Andrew-
Yes you are almost there. If the intent is to create a new route feature class with non-overlapping primary (dominant) routes only you'll want to add a few more steps to this process.
After the erase you'll most likely need to run multi-part to single part before you re-assemble the non-overlapping sections with the sections you've created as a result of the CRC process. After that, you'll append those two sources and then if you need to produce routes the easiest way to complete this would be to calculate MMin and MMax fields based on each feature part and then use the core linear referencing Create Routes GP tool.
Here's what my GP Model would look like:
The expression on the table view is: DominantFlag = 1 and DominantError <> 4
Hope that helps,
Amit @ Esri
Hello Andrew-
Yes you are almost there. If the intent is to create a new route feature class with non-overlapping primary (dominant) routes only you'll want to add a few more steps to this process.
After the erase you'll most likely need to run multi-part to single part before you re-assemble the non-overlapping sections with the sections you've created as a result of the CRC process. After that, you'll append those two sources and then if you need to produce routes the easiest way to complete this would be to calculate MMin and MMax fields based on each feature part and then use the core linear referencing Create Routes GP tool.
Here's what my GP Model would look like:
The expression on the table view is: DominantFlag = 1 and DominantError <> 4
Hope that helps,
Amit @ Esri
Thanks, Amit. This is very helpful.
My "model" is essentially the same as yours except that I'm using Merge/Dissolve rather than Multipart to Singlepart/Append. I've also done away with the template feature class, since all I need from the LRS is the route ID and geometry (the end game is simply to build a feature class of an external event table).
I'm still struggling to understand why you suggest creating new routes as a last step. Since I need the original m-values from the LRS to properly locate my external events, isn't it best to leave the native m-values?
Thanks again,
Andrew
Hello Andrew,
In my experience the Erase GP tool will typically create multi-part features. Reassembling the multi-part features with the output from Calculate Route Concurrency (CRC) dynamic segmentation using the merge/dissolve pattern will also continue to produce multi-part features where the part orders may be jumbled (Dissolve GP is mostly the common offender here). So although the geometry of the routes may be intact and the vertices are still carrying the original route m-values, the GP-derived route parts are not ordered correctly and would yield incorrect geometries if the features were used as input routes in Make Route Event Layer (MREL).
In order to create a monotonic derived LRS feature class where there are no route concurrencies and only the dominant routes are included I've found that exploding the erased multi-parts into single parts and then appending them to the erased routes preserves the part orders and m-values such that I can then field calculate the MMin and MMax for each singlepart and use those fields and polylines to build a monotonic network in core linear referencing.
This kind of derived network might be used for the purpose of say... feature based location of geometries that you wish to associate to the R&H LRS. I'm sure there are other reporting and publication uses as well where you wish to inform users or systems of only the routes that carry inventory/event information.
Regardless of the process you end up utilizing, I highly suggest you quality assure your derived network using the Data Reviewer Monotonicity Check in Polyline Checks.
Amit @ Esri
Amit,
Thanks so much for taking the time to write that up. It's a great explanation that likely saved me hours of debugging.
About 11,000 of my 195,000 routes are non-monotoic with my current workflow. I'll implement your suggestions and put the python script in here for others.
Cheers,
Andrew
Andrew - NCDOT has a SQL script we use that we can share with you.
Also, something important to state along with this topic - a route may be dominant along part of it's length and subordinate along a different part of it's length.
Erin,
Thanks for the response. I'd definitely be interested in taking a peak at the SQL script.
While we're on the subject of important things related to this topic, the Calculate Route Concurrency tool is only as good as your centerlines.
We currently have some duplicate centerlines in our LRS. Since we're constantly editing the LRS, we've been having a hard time working the Remove Duplicate Centerlines task into our business practice. The concurrent routes that are on duplicate centerlines will not show up as concurrencies in the CRC output.
Thanks,
Andrew
Amit,
I followed your model for creating a new route feature class with non-overlapping primary routes. I have discovered that this new feature classes m-values do not match the network it was derived from along all parts of a route. Smaller routes may match up in their entirety but longer routes my drift. In other words, the min and max m-values are fine, but the m-vales between the two networks my not match between the min and max m-values. The appended data in the network template feature class matches exactly, but after running the create routes geoprocessing tool from the MMin and MMax values the error is introduced. I may be able to eliminate the error by: taking the original network and running feature vertices to points, then locate features along routes on the output, then make route event layer, then add xy coordinates, then run delete identical based on routeid and xy coordinates, as well as, delete all points where RID does not match ROUTEID, and then finally use those points to calibrate the new primary routes network. This seems a bit convoluted and the processing time will take a couple of days. Any thoughts?
Thank you,
Justin.
Hi Justin - Interesting find. Can you validate whether the spatial parameters for the GP environment using Create Routes matches your ALRS?
Good day,
Just finished the process with the environmental settings locked down. I set the XY resolution and tolerance, also set the M resolution and tolerance to match the ALRS. Unfortunately, the error is still introduced after running the create routes geoprocessing tool. Do you see this on your end? The specific route I am looking at has a calibrated length of 13.09 miles and a geometric length of 12.876342 miles.
Thanks,
Justin.