Road Intersection Viewer

Introduction

I wrote the Python program Intersection_Viewer.py during a group project. The program generates a graphical representation of any road intersection from the data being used for the group project. The smallest angle between the roads at the intersection is identified and labeled. The graphics produced could be used in a report and Intersection_Viewer.py is faster than trying to produce similar results in ArcMap.

There is a text field in the lower left corner of the canvas that allows the identification number of an intersection to be entered for easy retrieval. The program retrieves the coordinates for the center of the intersection and coordinates along each road of the intersection from a table. The bearing that each road leaves the intersection is calculated and then used to plot a line for each road on a polar chart using matplotlib. Labels with the road identification number are placed on top of each road lines.

Output

Intersection_Viewer.py

import arcpy
import math
import matplotlib.pyplot as plt
from matplotlib.widgets import TextBox
import numpy as np
import operator

project_db = "E:/road_angle/Project.gdb"
arcpy.env.workspace = project_db

data_table = "Intersection_XY"
fields = ['RID', 'Center_X', 'Center_Y',
          'Road_X', 'Road_Y']


# Calculate the bearing in degrees of a vector that goes through xy2 and has
# the origin xy1 .
def get_bearing(xy1, xy2):
    x1, y1 = xy1
    x2, y2 = xy2
    dx = x2 - x1
    dy = y2 - y1
    radian = math.atan2(dy, dx)
    radian = radian - (math.pi / 2)
    if (radian > 0):
        degrees = 360 - (radian * 360) / (2 * math.pi)
    else:
        degrees = 360 - ((2 * math.pi + radian) * 360) / (2 * math.pi)
    return degrees


# Returns a list of tuples (road id number, bearing direction) that is sorted
# by bearing degree (least to greatest).
def get_road_bearings(intersection_id):
    where_clause = "IntersID = {}".format(intersection_id)
    road_bearings = []
    with arcpy.da.SearchCursor(data_table, fields, where_clause) as cursor:
        for row in cursor:
            rid, center_x, center_y, bearing_x, bearing_y = row
            bearing = get_bearing((center_x, center_y), (bearing_x, bearing_y))
            road_bearings.append((rid, bearing))
    road_bearings.sort(key=operator.itemgetter(1))
    return (road_bearings)


fig, ax = plt.subplots(subplot_kw=dict(polar=True))


# Returns the degrees to rotate the text labels so that they remain
# readable when placed along the length of the road.
def label_rotation(bearing):
    if 0 <= bearing <= 180:
        return 90 - bearing
    else:
        return 270 - bearing


# The function draw_chart clears the last chart and recreates it with a given
# intersection id (text).
def draw_chart(text):
    ax.cla()
    intersection = int(text)
    road_bearings = get_road_bearings(intersection)
    
    # If road_bearings is [] then the intersection id is likely wrong.
    if not road_bearings:
        print("There is not an intersection with that ID")
        road_bearings = [("- SUCH -", 45), ("- EXISTS -", 135),
                         ("- INTERSECTION -", 225), ("- NO -", 315)]
        title = "*Unknown Intersection ID*"
    else:
        title = "Intersection {}".format(intersection)
    
    # Add the title to the matplotlib window. Adjust the chart so it is
    # oriented and labeled like a compass.
    fig.canvas.set_window_title(title)
    ax.set(
            theta_zero_location='N',
            theta_direction=-1,
            xticklabels=['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'],
            yticklabels=[],
            yticks=[]
        )
    ax.spines['polar'].set_visible(False)
    
    # The list road_bearings is sorted from least to greatest according to
    # bearing. This sorting assures there exists no other roads between two
    # adjacent elments of the road_bearings list. To find the angle between
    # adjacent roads ir is only a matter of finding the difference between 
    # adjacent elements.
    rid_list, bearing_list = zip(*road_bearings)
    # to_end, Calculates the angle between the last and first elements
    angle_list = np.ediff1d(bearing_list,
                            to_end=360 - (bearing_list[-1] - bearing_list[0]))

    plot_item = [(r, b, a) for r, b, a 
                     in zip(rid_list, bearing_list, angle_list)]
    plot_item.sort(key=operator.itemgetter(2))
    
    # The smallest_angle list contains the index integer for the road(s) in
    # plot_item that has/have the smallest angle between the road next to it.
    smallest_angle = []
    # The first road will always be one of the smallest since the list is
    # sorted. Two or more roads are considered the smallest if they're are all
    # under .1 degree of each other.
    for index, x in enumerate(plot_item):
        if index == 0:
            smallest = x[2]
            smallest_angle.append(index)
        elif np.absolute(smallest - x[2]) < 0.1:
            smallest_angle.append(index)
            
   # Go through each elment of plot_item so that the roads, labels and smallest
   # angles are placed on the axes.
    for index, x in enumerate(plot_item):
        # The length of the road lines are randomly chosen within a range.
        # This was done to make the finished chart visually feel more natural.
        length = np.random.uniform(8, 10)
        theta = np.radians(x[1])
        
        # Add the road line to the chart.
        ax.plot((0, theta), (0, length), color='blue')
        
        if index in smallest_angle:
            bisect_radian = np.radians(x[2]/2)
            bisect_theta = theta + bisect_radian
            # The smallest angle bar has to be placed using an angle between
            # the two roads. It is then given the width of the angle between
            # the roads.
            ax.bar(bisect_theta, 7, width=np.radians(x[2]),
                   facecolor='white',
                   edgecolor='black')
            # Add the smallest angle label.
            ax.annotate(str(round(x[2],2)) + "°",
                    (bisect_theta, 8.5),
                    rotation=label_rotation(np.degrees(bisect_theta)),
                    ha='center',
                    va='center',
                    family='serif',
                    fontsize='small',
                    backgroundcolor='white'
            )
        # Add the road ID label along the road line.
        ax.annotate(str(x[0]),(theta, length/2),
                rotation=label_rotation(x[1]),
                ha='center',
                va='center',
                family='serif',
                fontsize='small',
                backgroundcolor='white'
                )
        
draw_chart(29453)
axbox = plt.axes([0.2, 0, 0.2, 0.06])
axbox.axis('off')
text_box = TextBox(axbox, 'ID Input: ', initial='29453')
text_box.on_submit(draw_chart)
fig.show()