This script utilizes the following libraries:
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
# 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
# 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]
}
The following object defines the styling to be used in the kml file.
Definition of the keys:
id : ID of objectshow.fields : Fields to be shown in tooltip. Note : field(s) must exist in the datafill.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 usedline.green/blue/red : Scale of color for lines. (Any value up to 1)line.width : Width of linefill.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 labelsNote all keys are optional and have defaults if not specified
# 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
}
)
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 kmlspeed : Calculate average speed for each vehicle, define styling and folder structure of kml, divide dataframe by average mph and create kmlrsu_tracks : Define styling and folder structure of kml, divide dataframe by rsu coverage and create kmldef 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)
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)
cv_writer(hourly=True, speed=False, rsu_tracks=False)
cv_rsu_writer()
print('Complete')