Part 2 of 2: Creating visualizations from BSM synthetic trajectories.

Note this process uses the output file from part 1

Imports and Constants

This script utilizes the following libraries:

  • Pandas 0.23 : to easily format and manipulate the data
  • encyclopedia 0.29 : for creating the visualizations as kml files.

Constants definition

TRACK_FILE: Location of "Basic Safety Messages to Synthetic Trajectories" file
OUT_DIR: Directy that output get written to
RSU_CYLINDER_DATA: RSU location data
RSU_CENTER_DATA: RSU center point data

In [1]:
# Standard
from datetime import timedelta, datetime
from operator import itemgetter

# External
import pandas as pd
from encyclopedia import KML, KML_Folder
from bits.time import unix2str
In [2]:
# Constants
TRACK_FILE = 'AMCDTrajectories.csv'  
OUT_DIR = 'examples/'  
RSU_CYLINDER_DATA = {
    'RSU ID': [87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87, 87,
               87, 87, 87, 87, 87, 87, 87, 87, 87, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88,
               88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89,
               89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 90, 90, 90, 90, 90, 90, 90, 90, 90,
               90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 91, 91, 91,
               91, 91, 91, 91, 91, 91, 91, 91, 91, 91, 91, 91, 91, 91, 91, 91, 91, 91, 91, 91, 91, 91, 91, 91, 91, 91, 91, 91, 91, 91,
               91, 91, 91, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92,
               92, 92, 92, 92, 92, 92, 92, 92, 92],
    'Latitude': [38.93274736, 38.9327063, 38.93258438, 38.9323853, 38.9321151, 38.93178201, 38.93139614, 38.93096922,
                 38.93051421, 38.93004495, 38.92957569, 38.92912069, 38.92869378, 38.92830793, 38.92797485, 38.92770468, 38.92750561,
                 38.92738369, 38.92734264, 38.92738369, 38.92750561, 38.92770468, 38.92797485, 38.92830793, 38.92869378, 38.92912069,
                 38.92957569, 38.93004495, 38.93051421, 38.93096922, 38.93139614, 38.93178201, 38.9321151, 38.9323853, 38.93258438,
                 38.9327063, 38.93083036, 38.9307893, 38.93066738, 38.9304683, 38.93019811, 38.92986501, 38.92947914, 38.92905222,
                 38.92859721, 38.92812795, 38.92765869, 38.92720369, 38.92677678, 38.92639093, 38.92605785, 38.92578767, 38.92558861,
                 38.92546669, 38.92542564, 38.92546669, 38.92558861, 38.92578767, 38.92605785, 38.92639093, 38.92677678, 38.92720369,
                 38.92765869, 38.92812795, 38.92859721, 38.92905222, 38.92947914, 38.92986501, 38.93019811, 38.9304683, 38.93066738,
                 38.9307893, 38.92656136, 38.9265203, 38.92639838, 38.9261993, 38.92592911, 38.92559601, 38.92521014, 38.92478322,
                 38.92432821, 38.92385895, 38.92338969, 38.92293469, 38.92250778, 38.92212193, 38.92178885, 38.92151867, 38.9213196,
                 38.92119769, 38.92115664, 38.92119769, 38.9213196, 38.92151867, 38.92178885, 38.92212193, 38.92250778, 38.92293469,
                 38.92338969, 38.92385895, 38.92432821, 38.92478322, 38.92521014, 38.92559601, 38.92592911, 38.9261993, 38.92639838,
                 38.9265203, 38.92358536, 38.92354431, 38.92342238, 38.9232233, 38.92295311, 38.92262001, 38.92223414, 38.92180722,
                 38.92135221, 38.92088295, 38.92041369, 38.91995869, 38.91953178, 38.91914592, 38.91881285, 38.91854267, 38.9183436,
                 38.91822169, 38.91818064, 38.91822169, 38.9183436, 38.91854267, 38.91881285, 38.91914592, 38.91953178, 38.91995869,
                 38.92041369, 38.92088295, 38.92135221, 38.92180722, 38.92223414, 38.92262001, 38.92295311, 38.9232233, 38.92342238,
                 38.92354431, 38.92111836, 38.92107731, 38.92095539, 38.9207563, 38.92048611, 38.92015302, 38.91976714, 38.91934022,
                 38.91888521, 38.91841595, 38.91794669, 38.91749169, 38.91706478, 38.91667892, 38.91634585, 38.91607567, 38.9158766,
                 38.91575469, 38.91571364, 38.91575469, 38.9158766, 38.91607567, 38.91634585, 38.91667892, 38.91706478, 38.91749169,
                 38.91794669, 38.91841595, 38.91888521, 38.91934022, 38.91976714, 38.92015302, 38.92048611, 38.9207563, 38.92095539,
                 38.92107731, 38.91786737, 38.91782631, 38.91770439, 38.9175053, 38.91723511, 38.91690202, 38.91651614, 38.91608922,
                 38.91563421, 38.91516495, 38.91469569, 38.91424069, 38.91381378, 38.91342792, 38.91309485, 38.91282467, 38.9126256,
                 38.91250369, 38.91246263, 38.91250369, 38.9126256, 38.91282467, 38.91309485, 38.91342792, 38.91381378, 38.91424069,
                 38.91469569, 38.91516495, 38.91563421, 38.91608922, 38.91651614, 38.91690202, 38.91723511, 38.9175053, 38.91770439,
                 38.91782631],
    'Longitude': [-77.24315, -77.2425492, -77.24196666, -77.24142007, -77.24092606, -77.24049962, -77.24015372,
                  -77.23989886, -77.2397428, -77.23969026, -77.23974284, -77.23989895, -77.24015383, -77.24049975, -77.24092618,
                  -77.24142019, -77.24196674, -77.24254924, -77.24315, -77.24375076, -77.24433326, -77.24487981, -77.24537382,
                  -77.24580025, -77.24614617, -77.24640105, -77.24655716, -77.24660974, -77.2465572, -77.24640114, -77.24614628,
                  -77.24580038, -77.24537394, -77.24487993, -77.24433334, -77.2437508, -77.241327, -77.24072622, -77.24014369,
                  -77.23959712, -77.23910312, -77.23867669, -77.2383308, -77.23807595, -77.23791989, -77.23786735, -77.23791993,
                  -77.23807604, -77.23833091, -77.23867682, -77.23910324, -77.23959723, -77.24014377, -77.24072626, -77.241327,
                  -77.24192774, -77.24251023, -77.24305677, -77.24355076, -77.24397718, -77.24432309, -77.24457796, -77.24473407,
                  -77.24478665, -77.24473411, -77.24457805, -77.2443232, -77.24397731, -77.24355088, -77.24305688, -77.24251031,
                  -77.24192778, -77.236135, -77.23553425, -77.23495176, -77.23440522, -77.23391125, -77.23348485, -77.23313898,
                  -77.23288415, -77.23272809, -77.23267556, -77.23272814, -77.23288423, -77.23313909, -77.23348498, -77.23391138,
                  -77.23440534, -77.23495184, -77.2355343, -77.236135, -77.2367357, -77.23731816, -77.23786466, -77.23835862,
                  -77.23878502, -77.23913091, -77.23938577, -77.23954186, -77.23959444, -77.23954191, -77.23938585, -77.23913102,
                  -77.23878515, -77.23835875, -77.23786478, -77.23731824, -77.23673575, -77.234304, -77.23370328, -77.23312081,
                  -77.23257429, -77.23208034, -77.23165396, -77.2313081, -77.23105328, -77.23089724, -77.2308447, -77.23089728,
                  -77.23105337, -77.23130822, -77.23165409, -77.23208047, -77.23257441, -77.23312089, -77.23370332, -77.234304,
                  -77.23490468, -77.23548711, -77.23603359, -77.23652753, -77.23695391, -77.23729978, -77.23755463, -77.23771072,
                  -77.2377633, -77.23771076, -77.23755472, -77.2372999, -77.23695404, -77.23652766, -77.23603371, -77.23548719,
                  -77.23490472, -77.230494, -77.2298933, -77.22931085, -77.22876435, -77.22827042, -77.22784405, -77.22749821,
                  -77.22724339, -77.22708735, -77.22703482, -77.2270874, -77.22724348, -77.22749832, -77.22784418, -77.22827055,
                  -77.22876447, -77.22931093, -77.22989334, -77.230494, -77.23109466, -77.23167707, -77.23222353, -77.23271745,
                  -77.23314382, -77.23348968, -77.23374452, -77.2339006, -77.23395318, -77.23390065, -77.23374461, -77.23348979,
                  -77.23314395, -77.23271758, -77.23222365, -77.23167715, -77.2310947, -77.226364, -77.22576332, -77.2251809,
                  -77.22463443, -77.22414052, -77.22371417, -77.22336834, -77.22311354, -77.22295751, -77.22290498, -77.22295755,
                  -77.22311363, -77.22336846, -77.2237143, -77.22414065, -77.22463455, -77.22518099, -77.22576337, -77.226364,
                  -77.22696463, -77.22754701, -77.22809345, -77.22858735, -77.2290137, -77.22935954, -77.22961437, -77.22977045,
                  -77.22982302, -77.22977049, -77.22961446, -77.22935966, -77.22901383, -77.22858748, -77.22809357, -77.2275471,
                  -77.22696468]
}
RSU_CENTER_DATA = {
    'RSU ID': [87, 88, 89, 90, 91, 92],
    'Latitude': [38.930045, 38.928128, 38.923859, 38.920883, 38.918416, 38.915165],
    'Longitude': [-77.24315, -77.241327, -77.236135, -77.234304, -77.230494, -77.226364]
}

Styling

The following object defines the styling to be used in the kml file.
Definition of the keys:

  • id : ID of object
  • show.fields : Fields to be shown in tooltip. Note : field(s) must exist in the data
  • fill.on : Specifies polygon fills (True or False)
  • icon.green/blue/red : Scale of color for icons. (Any value up to 1)
  • icon.shape : KML Placemark icon to be used
  • line.green/blue/red : Scale of color for lines. (Any value up to 1)
  • line.width : Width of line
  • fill.opacity : Opacity of polygon fills (Any value up to 1)
  • fill.green/blue/red : Scale of color for fill (Any value up to 1)
  • label.scale : Size of labels

Note all keys are optional and have defaults if not specified

In [3]:
# Specifies what fields will be displayed in kml tooltip (can be any field(s) in data).
META = ['id']

# Base style of kml
STYLE = KML.Style(
    {
        'id': 'track-style',
        'show.fields': META,
        'fill.on': True,
        'icon.green': 0,
        'icon.blue': 0,
        'icon.red': 0,
        'icon.opacity': 0,
        'icon.shape': 'http://maps.google.com/mapfiles/kml/shapes/road_shield3.png',
        'line.green': 0,
        'line.blue': 0,
        'line.red': 0,
        'line.width': 3,
        'fill.opacity': 0,
        'fill.green': 0,
        'fill.blue': 0,
        'fill.red': 0,
        'label.scale': 0
    }
)

cv_writer(hourly=True, speed=False, rsu_tracks=False)

Creates KML of BSM trajectory files
  • hourly : Calculate average speed for each vehicle, define styling and folder structure of kml, divide dataframe by hour, further group hourly dataframes by average mph and create kml
  • speed : Calculate average speed for each vehicle, define styling and folder structure of kml, divide dataframe by average mph and create kml
  • rsu_tracks : Define styling and folder structure of kml, divide dataframe by rsu coverage and create kml
In [4]:
def cv_writer(hourly=True, speed=False, rsu_tracks=False):
    """
    Creates KML of connected vehicle (cv) trajectory files

    - hourly: Creates hourly subfolders
    - speed: Creates speed subfolders
    - rsu_tracks: Creates rsu coverage subfolders
    """

    print('Drawing Tracks ...')

    # One of the following arguments must be true
    assert(hourly or speed or rsu_tracks)

    # Define output filename
    file_name = TRACK_FILE.split('.csv')[-2] + '.KML'
    file_name = OUT_DIR + file_name.split('/')[-1]

    # Store data in dataframe
    tracks_df = pd.read_csv(TRACK_FILE)
    tracks_df = tracks_df[tracks_df.groupby('id')['id'].transform('size') > 1] # Remove single points
    tracks_df['uid'] = tracks_df['id'] # Add 'uid' column (Required field for Tracks class)
    tracks_df.rename(columns={'long': 'lon', 'tic':'tick'}, inplace=True) # Convert to encyclopedia format
    tracks_df = tracks_df.sort_values(['id', 'tick']) # Sort df by id and time (tick)
    
    # Open kml file
    with KML_Folder(filename=file_name) as kt:
        # Specify geometry type and altitude mode
        kt['Document', 'geometry'] = 'gx:Track'
        kt['Document', 'altmode'] = 'relativeToGround'
            
        # Subdivides tracks by hour and speed, creating subfolders in kml
        if hourly:
            print('Creating Hourly Folder ...')

            # Convert speed from fps to mph
            tracks_df['speed_mph'] = tracks_df['speed'] * .681818
            tracks_df = tracks_df.join(tracks_df.groupby('id')['speed_mph'].mean(), on='id', rsuffix='_avg')

            # Defines new meta, adding average speed
            speed_meta = META + ['speed_mph_avg']

            # Define track styles, inherrited from base 'style'
            vehicle_style = STYLE
            kt.stylize({**vehicle_style, 'line.red': 1, 'show.fields': speed_meta, 'id': 'slow-track-style'})
            kt.stylize({**vehicle_style, 'line.green': 1, 'line.red': 1, 'show.fields': speed_meta, 'id':'moderate-track-style'})
            kt.stylize({**vehicle_style, 'icon.green': 1, 'line.green': 1, 'show.fields': speed_meta, 'id': 'fast-track-style'})

            # Creation of new 'Hourly Tracks' Folder
            kt['Document'] = hourly = kt.unique('Hourly Tracks')

            # Make new df every hour
            min_time = tracks_df['tick'].min()
            max_time = tracks_df['tick'].max()
            current_min_time = min_time
            current_max_time = current_min_time + 3600

            while current_min_time <= max_time:
                hour_df = tracks_df[(tracks_df['tick'] >= current_min_time) & (tracks_df['tick'] <= current_max_time)]

                # Convert tick to UTC time format
                min_utc = str(datetime.strptime(unix2str(current_min_time, format='%H:%M:%S'), '%H:%M:%S') - timedelta(hours=5))
                min_utc = min_utc.split()[1]
                max_utc = str(datetime.strptime(unix2str(current_max_time, format='%H:%M:%S'), '%H:%M:%S') - timedelta(hours=5))
                max_utc = max_utc.split()[1]

                # Creates subfolders for every hour
                kt[hourly] = hour = kt.unique(min_utc + ' - ' + max_utc)

                # Groups tracks by average speed
                slow_df = hour_df[hour_df['speed_mph_avg'] < 10]
                mod_df = hour_df[(hour_df['speed_mph_avg'] >= 10) & (hour_df['speed_mph_avg'] <= 20)]
                fast_df = hour_df[hour_df['speed_mph_avg'] > 20]

                # Converts dataframes to lists
                slow_tracks = slow_df.to_dict('records')
                mod_tracks = mod_df.to_dict('records')
                fast_tracks = fast_df.to_dict('records')

                # Subdivides folder structure further by speed category and styles tracks according to speed

                kt[hour] = st = kt.unique('< 10 mph')
                kt[st, 'styleUrl'] = 'slow-track-style'
                kt[hour] = mt = kt.unique('10 mph - 20 mph')
                kt[mt, 'styleUrl'] = 'moderate-track-style'
                kt[hour] = ft = kt.unique('> 20 mph')
                kt[ft, 'styleUrl'] = 'fast-track-style'

                # Writes kml data
                kt.draw(st, slow_tracks)
                kt.draw(mt, mod_tracks)
                kt.draw(ft, fast_tracks)

                # Shift to next hour
                current_min_time = current_max_time
                current_max_time += 3600
                
        # Subdivides tracks only by speed, creating subfolders in kml
        if speed:
            print('Creating Speed Breakdown Folder ...')

            # Prevents duplicate executions that were already performed in Hourly branch
            if not hourly:
                tracks_df['speed_mph'] = tracks_df['speed'] * .681818 # Convert speed from fps to mph
                tracks_df = tracks_df.join(tracks_df.groupby('id')['speed_mph'].mean(), on='id', rsuffix='_avg')

                speed_meta = META + ['speed_mph_avg']

                vehicle_style = STYLE
                kt.stylize({**vehicle_style, 'line.red': 1, 'show.fields': speed_meta, 'id': 'slow-track-style'})
                kt.stylize({**vehicle_style, 'line.green': 1, 'line.red': 1, 'show.fields': speed_meta, 'id':'moderate-track-style'})
                kt.stylize({**vehicle_style, 'icon.green': 1, 'line.green': 1, 'show.fields': speed_meta, 'id': 'fast-track-style'})

            # Creation of new 'Speed Breakdown' Folder
            kt['Document'] = speed = kt.unique('Speed Breakdown')

            # Groups tracks by average speed
            slow_df = tracks_df[tracks_df['speed_mph_avg'] < 10]
            mod_df = tracks_df[(tracks_df['speed_mph_avg'] >= 10) & (tracks_df['speed_mph_avg'] <= 20)]
            fast_df = tracks_df[tracks_df['speed_mph_avg'] > 20]

            # Converts dataframe to Tracks object
            slow_tracks = slow_df.to_dict('records')
            mod_tracks = mod_df.to_dict('records')
            fast_tracks = fast_df.to_dict('records')

            # Subdivides folder structure further by speed category and styles tracks according to speed
            kt[speed] = st = kt.unique('< 10 mph')
            kt[st, 'styleUrl'] = 'slow-track-style'
            kt[speed] = mt = kt.unique('10 mph - 20 mph')
            kt[mt, 'styleUrl'] = 'moderate-track-style'
            kt[speed] = ft = kt.unique('> 20 mph')
            kt[ft, 'styleUrl'] = 'fast-track-style'

            # Writes kml data
            kt.draw(st, slow_tracks)
            kt.draw(mt, mod_tracks)
            kt.draw(ft, fast_tracks)
            
        # Subdivides tracks according to their RSU coverage, creating subfolders in kml
        if rsu_tracks:
            print('Drawing RSU Coverage Tracks ...')

            # Defines styles for vehicles in and out of coverage
            vehicle_style = STYLE
            kt.stylize({**vehicle_style, 'line.blue': 1, 'line.green': 1, 'label.scale': 0, 'id': 'in-range-style'})
            kt.stylize({**vehicle_style, 'line.blue': 1, 'label.scale': 0, 'id': 'out-of-range-style'})

            # Creation of new 'Communication Breakdown' Folder
            kt['Document'] = rsu = kt.unique('Communication Breakdown')

            # Subdivides folder structure further by coverage category and styles tracks accordingly
            kt[rsu] = ir = kt.unique('In Coverage')
            kt[ir, 'styleUrl'] = 'in-range-style'
            kt[rsu] = oor = kt.unique('Out of Coverage')
            kt[oor, 'styleUrl'] = 'out-of-range-style'

            # Adds required 'uid' field and converts tracks to sorted list of dictionaries
            tracks_df['uid'] = 0
            rsu_dicts = tracks_df.T.to_dict().values()
            rsu_dicts = list(rsu_dicts)
            rsu_dicts = sorted(rsu_dicts, key=itemgetter('id', 'tick'))

            # Sets new uid values according to coverage
            first = True
            prev_row = {}
            uid_count = 0
            for row in rsu_dicts:
                if first:
                    prev_row = row
                    first = False
                    continue
                # Change uid when new id appears
                elif row['id'] != prev_row['id']:
                    uid_count += 1
                    row['uid'] = uid_count
                # Change uid when the value of 'inrangeofrsu' changes
                elif row['inrangeofrsu'] != prev_row['inrangeofrsu']:
                    uid_count += 1
                    row['uid'] = uid_count
                # RSU coverage didn't change. UID remains the same
                else:
                    row['uid'] = prev_row['uid']
                prev_row = row

            # Stores dict in dataframe and sorts by id and time
            rsu_df = pd.DataFrame(rsu_dicts)
            rsu_df = rsu_df.sort_values(['id', 'tick'])

            # Groups tracks according to RSU coverage
            in_range_df = rsu_df[rsu_df['inrangeofrsu'] is True]
            out_of_range_df = rsu_df[rsu_df['inrangeofrsu'] is False]

            # Converts dataframe to Tracks object
            in_range_tracks = in_range_df.to_dict('records')
            out_of_range_tracks = out_of_range_df.to_dict('records')

            # Writes kml data
            kt.draw(ir, in_range_tracks)
            kt.draw(oor, out_of_range_tracks)

cv_rsu_writer(rsu=False, rsu_points=True)

Creates KML of Roadside Unit (RSU) locations
  • rsu : Creates 3D cylinders of RSUs
  • rsu_points : Draws center points of RSUs
In [5]:
def cv_rsu_writer(rsu=False, rsu_points=True):
    """
    Creates KMLs of Roadside Unit (RSU) locations (as 3D cylinders or Spheres)
    """

    # One of the following arguments must be true
    assert(rsu or rsu_points)

    if rsu:
        print('Drawing RSUs ...')

        # Get max altitude from tracks
        tracks_df = pd.read_csv(TRACK_FILE)
        tracks_df = tracks_df[tracks_df.groupby('id')['id'].transform('size') > 1] # Remove single points
        t_max = tracks_df['alt'].max() # Max altitude

        # Read RSU locations file
        rsu_df = pd.DataFrame(RSU_CYLINDER_DATA)
        rsu_df.rename(columns={'RSU ID': 'id', 'Latitude': 'lat', 'Longitude': 'lon'}, inplace=True)
        rsu_df['uid'] = rsu_df['id']
        rsu_df['alt'] = t_max

        rsu_groups = rsu_df.groupby('id')
        rsu_df = pd.DataFrame([])

        # Append first point of coordinates the end for complete polygons (Data was missing)
        for id, group in rsu_groups:
            first_row = group.iloc[0]
            group = group.append(first_row, ignore_index=True)
            rsu_df = rsu_df.append(group, ignore_index=True)

        file_name = OUT_DIR + 'rsu_cylinders.kml'

        with KML_Folder(filename=file_name) as kt:
            cylinder_style = STYLE
            kt.stylize({**cylinder_style, 'line.red': 1, 'line.green': .55, 'line.width':1, 'id': 'blue-cylinder-style'})

            cylinder_points = rsu_df.to_dict('records')

            kt['Document', 'styleUrl'] = 'blue-cylinder-style'
            kt['Document', 'geometry'] = 'Polygon'
            kt['Document', 'altmode'] = 'relativeToGround'
            kt['Document', 'extrude'] = True

            kt['Document'] = r = 'RSUs'

            kt.draw(r, cylinder_points)

    # Creates KML of RSU center locations (as red spheres)
    if rsu_points:
        print('Drawing RSUs Centers...')

        # Get max altitude from tracks
        tracks_df = pd.read_csv(TRACK_FILE)
        tracks_df = tracks_df[tracks_df.groupby('id')['id'].transform('size') > 1] # Remove single points
        t_max = tracks_df['alt'].max() # Max altitude

        # Read RSU locations file
        rsu_df = pd.DataFrame(RSU_CENTER_DATA)
        rsu_df.rename(columns={'RSU ID': 'id', 'Latitude': 'lat', 'Longitude': 'lon'}, inplace=True)
        rsu_df['uid'] = rsu_df['id']
        rsu_df['alt'] = t_max

        file_name = OUT_DIR + 'rsu_centers.kml'

        with KML_Folder(filename=file_name) as kt:
            center_style = STYLE
            kt.stylize({**center_style, 'icon.red': 1, 'icon.green': .5, 'icon.opacity': 1, 'icon.scale': 1, 'id': 'orange-center-style'})

            center_points = rsu_df.to_dict('records')

            kt['Document', 'styleUrl'] = 'orange-center-style'
            kt['Document', 'geometry'] = 'Point'
            kt['Document', 'altmode'] = 'relativeToGround'
            kt['Document', 'extrude'] = True

            kt['Document'] = r = 'RSUs'

            kt.draw(r, center_points)

Run cv_writer()

In [6]:
cv_writer(hourly=True, speed=False, rsu_tracks=False)
cv_rsu_writer()
print('Complete')
Drawing Tracks ...
Creating Hourly Folder ...
Drawing RSUs Centers...
Complete