Setting: A multi-day tech conference in September 2024 is hosted by the City of Chicago. The city hopes to provide a seemless and impressive experience for visitors by building a dedicated webpage for expo atendees
Goal: Develop a website that dynamically routes individual’s to upcoming expo events and highlights local amenities
Deliverable: Python code that utilizes the cities database of events to dynamically route an individual to nearby events given their location and the time of day, highlighting points of interest and the city character in an interactive web map
Environment: The expo events and their associated conference centers are saved in a PostgreSQL database. The city has requested that an open source solution be developed which will integrate with existing systems.
Created and Retrieved Data
Created PostgreSQL database and saved datasets
Visualized event, POI, shapefile data in QGIS
Wrote Spatial SQL code to query nearby POI, upcoming expo events and close by shapefiles
Set up Google Maps API and wrote code to compute walking directions between user location and next expo event
Used Folium to create interactive HTML maps
Used Flask to create a webapp for dynamic creation of maps
# import leaflet, pandas, geospatial packages
import folium
import pandas as pd
import geopandas as gpd
import os
import numpy as np
import matplotlib.pyplot as plt
import sys
import json
import requests
import osmnx as ox
import re
import time
import random
from datetime import datetime, timedelta
import base64
# Import packages to connect to postgres
from sqlalchemy import create_engine
from sqlalchemy_utils import database_exists, create_database
import psycopg2
import googlemaps
from folium.features import GeoJsonTooltip
from shapely.geometry import LineString, MultiLineString
import polyline
from shapely import wkt
# load config.py from ../utils/config.py
sys.path.append('utils')
sys.path.append('/Users/coltonlapp/Dropbox/My Mac (Coltons-MacBook-Pro.local)/Desktop/SCHOOL/Year2_Spring/Spatial DataScience/NearestPointOfInterest/code/utils')
from config import *
# create connection
def connect_to_db():
# Use the variables in config.py to connect to the database
dbname = DBNAME
username = USERNAME
# host is localhost
host = HOST
# password is your postgres password
password = PGPASS
# connect
try:
con = psycopg2.connect(database = dbname, user = username, host = host, password = password)
return con
except:
print("Unable to connect to the database")
return None
con = connect_to_db()
# Define parameters in a dictionary
chicago_params = {
"min_lat": 41.8708,
"max_lat": 41.8920,
"min_lon": -87.635,
"max_lon": -87.615,
"start_datetime": datetime(2024, 9, 20, 17, 0), # Friday 5pm
"end_datetime": datetime(2024, 9, 22, 22, 0) # Sunday 10pm
}
def get_random_location_time(params):
# Generate a random latitude and longitude within the bounding box
random_lat = random.uniform(params["min_lat"], params["max_lat"])
random_lon = random.uniform(params["min_lon"], params["max_lon"])
# Calculate the difference in seconds between the start and end datetimes
time_difference = (params["end_datetime"] - params["start_datetime"]).total_seconds()
# generate a time between 8am and 8pm using while loop
while True:
random_seconds = random.uniform(0, time_difference)
random_datetime = params["start_datetime"] + timedelta(seconds=random_seconds)
if random_datetime.hour >= 8 and random_datetime.hour < 20:
break
# Round time to nearest 15 minutes
random_datetime = random_datetime - timedelta(minutes=random_datetime.minute % 15,
seconds=random_datetime.second,
microseconds=random_datetime.microsecond)
# Create pandas df with random location and time
random_data = pd.DataFrame({
"datetime": [random_datetime],
"lat": [random_lat],
"lon": [random_lon]
})
# Turn into gdf
user_time_location = gpd.GeoDataFrame(random_data, geometry=gpd.points_from_xy(random_data.lon, random_data.lat))
return user_time_location
def return_close_POI(user_time_location, n_miles = 1, n_return = 10):
osm_poi_table_name = 'osm_poi'
# user point WKT
point_wkt = user_time_location.geometry.iloc[0].wkt
osm_query = f"""
SELECT *,
ST_Distance(ST_Transform(geom, 26986), ST_Transform(ST_GeomFromText('{point_wkt}', 4326), 26986)) / 1609.34 AS distance_miles
FROM {osm_poi_table_name}
WHERE ST_DWithin(ST_Transform(geom, 26986), ST_Transform(ST_GeomFromText('{point_wkt}', 4326), 26986), {n_miles} * 1609.34);
"""
# Execute the query and load the results into a GeoDataFrame
with con.cursor() as cursor:
cursor.execute(osm_query)
rows = cursor.fetchall()
# Fetch the column names from the cursor
colnames = [desc[0] for desc in cursor.description]
# Create the GeoDataFrame using the fetched column names
result_gdf = gpd.GeoDataFrame(rows, columns=colnames)
# sort gdf by distance_miles
result_gdf = result_gdf.sort_values(by='distance_miles')
# return top n_return results
#result_gdf = result_gdf.head(n_return)
# Return random 10 subset
result_gdf = result_gdf.sample(n_return)
return result_gdf
# given users datetime, query database to get next events and venue information
def get_next_events(user_time_location, n_return = 5):
events_table_name = 'events'
venues_table_name = 'event_spaces_subset'
# user point WKT
point_wkt = user_time_location.geometry.iloc[0].wkt
# Query to get n_return next events based off users time, and calculate distance to user. Join events and venues
events_query = f"""
SELECT e.*, v.*,
ST_Distance(ST_Transform (v.geom, 26986), ST_Transform(ST_GeomFromText('{point_wkt}', 4326), 26986)) / 1609.34 AS distance_miles
FROM {events_table_name} e
JOIN {venues_table_name} v ON e.venueid = v.id
WHERE e.event_start > '{user_time_location.datetime.iloc[0]}'::timestamp AT TIME ZONE 'America/Chicago'
ORDER BY e.starttime
LIMIT {n_return};
"""
# Execute the query and load the results into a GeoDataFrame
with con.cursor() as cursor:
cursor.execute(events_query)
rows = cursor.fetchall()
# Fetch the column names from the cursor
colnames = [desc[0] for desc in cursor.description]
# Create the GeoDataFrame using the fetched column names
result_gdf = gpd.GeoDataFrame(rows, columns=colnames)
# sort by event start time
result_gdf = result_gdf.sort_values(by='event_start')
return result_gdf
def get_parks_and_waterways(user_time_location, n_miles = 2):
parks_table_name = 'chi_parks'
waterways_table_name = 'Chi_water'
# user point WKT
point_wkt = user_time_location.geometry.iloc[0].wkt
# query parks within 2 miles of user
parks_query = f"""
SELECT
*,
ST_AsText(geom) AS geom_wkt,
ST_Distance(
ST_Transform(geom, 26986),
ST_Transform(ST_GeomFromText('{point_wkt}', 4326), 26986)
) / 1609.34 AS distance_miles
FROM
{parks_table_name}
WHERE
ST_DWithin(
ST_Transform(geom, 26986),
ST_Transform(ST_GeomFromText('{point_wkt}', 4326), 26986),
2 * 1609.34
);
"""
# query waterways within 5 miles of user
waterways_query = f"""
SELECT
*,
ST_AsText(geom) AS geom_wkt,
ST_Distance(
ST_Transform(geom, 26986),
ST_Transform(ST_GeomFromText('{point_wkt}', 4326), 26986)
) / 1609.34 AS distance_miles
FROM
{waterways_table_name}
WHERE
ST_DWithin(
ST_Transform(geom, 26986),
ST_Transform(ST_GeomFromText('{point_wkt}', 4326), 26986),
5 * 1609.34
);
"""
# Execute the query and load the results into a GeoDataFrame
with con.cursor() as cursor:
# Execute parks query and fetch data
cursor.execute(parks_query)
rows = cursor.fetchall()
colnames = [desc[0] for desc in cursor.description]
parks_df = pd.DataFrame(rows, columns=colnames)
# Convert WKT in 'geom_wkt' to shapely Geometries
if 'geom_wkt' in parks_df.columns:
parks_df['geom'] = parks_df['geom_wkt'].apply(wkt.loads)
parks_df.drop(columns=['geom_wkt'], inplace=True) # Optional: remove the WKT column
# Create GeoDataFrame
parks_gdf = gpd.GeoDataFrame(parks_df, geometry='geom')
# Execute waterways query and fetch data
cursor.execute(waterways_query)
rows = cursor.fetchall()
colnames = [desc[0] for desc in cursor.description]
waterways_df = pd.DataFrame(rows, columns=colnames)
if 'geom_wkt' in waterways_df.columns:
waterways_df['geom'] = waterways_df['geom_wkt'].apply(wkt.loads)
waterways_df.drop(columns=['geom_wkt'], inplace=True) # Optional: remove the WKT column
# Create GeoDataFrame
waterways_gdf = gpd.GeoDataFrame(waterways_df, geometry='geom')
return parks_gdf, waterways_gdf
def get_comms():
table_name = 'chi_comm'
# query to get all community areas
comm_query = f""" SELECT *, ST_AsText(geom) AS geom_wkt
FROM {table_name}
"""
# Execute the query and load the results into a GeoDataFrame
with con.cursor() as cursor:
cursor.execute(comm_query)
rows = cursor.fetchall()
colnames = [desc[0] for desc in cursor.description]
comm_df = pd.DataFrame(rows, columns=colnames)
if 'geom_wkt' in comm_df.columns:
comm_df['geom'] = comm_df['geom_wkt'].apply(wkt.loads)
comm_df.drop(columns=['geom_wkt'], inplace=True) # Optional: remove the WKT column
# Create GeoDataFrame
comm_gdf = gpd.GeoDataFrame(comm_df, geometry='geom')
return comm_gdf
def get_route_to_next_event( user_lat_lon, event_lat_lon):
gmaps = googlemaps.Client(key=GMAPS_API_KEY)
# Request walking directions
directions_result = gmaps.directions(
user_lat_lon,
event_lat_lon,
mode="walking",
departure_time=datetime.now()
)
# Extract the polyline from the response
if directions_result:
directions_html = [ i['html_instructions'] for i in directions_result[0]['legs'][0]['steps'] ]
directions_text = [re.sub('<[^<]+?>', '', i) for i in directions_html]
polyline_list = [ l['polyline']['points'] for l in directions_result[0]['legs'][0]['steps'] ]
multiline_points = []
for pl in polyline_list:
# Decode polyline string into tuple of coordinates
decoded_points = polyline.decode(pl)
multiline_points.extend(decoded_points)
return {'directions_html': directions_html,
'directions_text': directions_text,
'route_points': multiline_points,
'api_results': directions_result}
def get_div_icon_html(type, text_dict):
if type=='user_location':
# usage: text_dict['time'] = row['datetime'].strftime('%I:%M %p')
html = """ <div style="position: relative; text-align: center; width: 70px;">
<div style="position: absolute; top: -10px; left: 0; width: 100%; font-weight: bold; font-family: Arial; color: black;">
User Location
</div>
<svg width="70" height="70">
<polygon points="35,5 41,25 55,25 45,35 50,55 35,45 20,55 25,35 15,25 29,25"
style="fill: yellow; stroke: black; stroke-width: 1;"/>
<text x="50%" y="65" text-anchor="middle" font-family="Arial" font-size="10" fill="black">Time: {} </text>
</svg>
</div>
""".format( text_dict['time'])
elif type=='event':
html = """
<div style="position: relative; text-align: center; width: 100px;">
<img src="data:image/png;base64,{expo_logo_64}" width="40" height="40" style="margin-top: 0px;"/>
<div style="position: absolute; top: 35px; left: 0; width: 100%; font-family: Arial;
color: black; text-shadow: 1px 1px 1px #fff; background-color: rgba(255, 255, 255, 0.3);">
{event_title} - {event_time}
</div>
</div>
""".format(expo_logo_64=text_dict['expo_logo_64'],
event_title=text_dict['event_title'] ,
event_time=text_dict['event_time'] )
return html
# Create leaflet map of chicago with random location
def create_map():
# Create a map centered on Chicago
m = folium.Map(location=[41.8781, -87.6298], zoom_start=15, width='75%', height='75%')
# ----------------- Add user location ----------------- #
# Get random point in chicago and add to map
user_time_location = get_random_location_time(chicago_params)
# Add the random point to the map, with a popup saying "User location, time, lat/lon"
for idx, row in user_time_location.iterrows():
icon = folium.DivIcon(icon_anchor=(35, 34), html= get_div_icon_html( 'user_location', {'time' :row['datetime'].strftime('%I:%M %p') } ) )
folium.Marker(location=[row["lat"], row["lon"]], icon=icon).add_to(m)
# ----------------- Add nearby POI ----------------- #
# get closest POI
closest_poi = return_close_POI(user_time_location, n_return = 25)
# keep max of 5 of each amenity type in closest poi
for amenity_type in closest_poi['amenity'].unique():
subset = closest_poi[closest_poi['amenity'] == amenity_type]
closest_poi = closest_poi.drop(subset.index[5:])
# Create a FeatureGroup to hold the POI markers
feature_group = folium.FeatureGroup(name="Nearby Points of Interest")
for amenity_type in closest_poi['amenity'].unique():
subset = closest_poi[closest_poi['amenity'] == amenity_type]
logo_path = os.path.join(LOGO_DIR, f'{amenity_type}.png')
for idx, row in subset.iterrows():
icon = folium.CustomIcon(icon_image=logo_path, icon_size=(30, 30))
popup_content = f"{row['name']} - {row['amenity'].capitalize()}<br>{row['address']}<br>{row['distance_miles']:.2f} miles<br>{row['phone']}"
popup = folium.Popup(popup_content, max_width=300)
folium.Marker(location=[row["lat"], row["lon"]], icon=icon, popup=popup).add_to(feature_group)
# Add the FeatureGroup to the map
feature_group.add_to(m)
# ----------------- Add upcoming Expo events ----------------- #
# get next events
events = get_next_events(user_time_location)
# Encode the binary data in base64
expo_logo_path = os.path.join(LOGO_DIR, 'expo_logo.png')
with open(expo_logo_path, 'rb') as f:
image_data = f.read()
expo_logo_64 = base64.b64encode(image_data).decode('utf-8')
# Add the events to the map
labels = folium.FeatureGroup(name='Upcoming Expo Events')
for idx, row in events.iterrows():
icon = folium.DivIcon(icon_anchor=(50, 20),
html= get_div_icon_html( 'event', {'expo_logo_64': expo_logo_64,
'event_title' : row['eventtitle'],
'event_time' : row['event_start'].strftime('%I:%M %p') }) )
folium.Marker(location=[row["lat"], row["lon"]],
icon=icon).add_to(labels) # Add to the FeatureGroup
labels.add_to(m)
# ----------------- Add route to closest event ----------------- #
# subset closest event
if len(events) != 0:
# get closest event after sorting events by distance_miles
events = events.sort_values(by='distance_miles')
closest_event = events.iloc[0]
if len(closest_event) != 0:
found_event = True
event_lat_lon = (closest_event['lat'], closest_event['lon'])
user_lat_lon = (user_time_location.loc[0, 'lat'], user_time_location.loc[0, 'lon'])
# Get route to closest event
directions_result = get_route_to_next_event(user_lat_lon, event_lat_lon)
# Add the route to the map
labels = folium.FeatureGroup(name='Route to Closest Event')
folium.PolyLine(directions_result['route_points'], color="blue", weight=2.5, opacity=1).add_to(m)
labels.add_to(m)
else:
found_event = False
directions_result = None
closest_event = None
# ----------------- Add Chicago Parks and waterways ----------------- #
parks_gdf, waterways_gdf = get_parks_and_waterways(user_time_location)
# Create a FeatureGroup to hold the park polygons
feature_group = folium.FeatureGroup(name="Chicago Parks")
# add parks as green fill no outline with on hover popup of "label" column
for idx, row in parks_gdf.iterrows():
folium.GeoJson(
data=row['geom'],
style_function=lambda x: {'fillColor': 'green', 'fillOpacity': 0.3, 'color': 'none'}, # Set outline color to 'none'
highlight_function=lambda x: {'weight': 3, 'fillOpacity': 0.5}, tooltip=row['label']).add_to(feature_group)
# Add the FeatureGroup to the map
feature_group.add_to(m)
# Create a FeatureGroup to hold the waterway lines
feature_group = folium.FeatureGroup(name="Chicago Waterways")
# add waterways as blue lines with on hover popup of "label" column
for idx, row in waterways_gdf.iterrows():
folium.GeoJson(
data=row['geom'],
style_function=lambda x: {'fillColor': 'blue', 'fillOpacity': 0.2, 'color': 'none'},
highlight_function=lambda x: {'weight': 1, 'color': 'blue'}, tooltip=row['name']).add_to(feature_group)
# Add the FeatureGroup to the map
feature_group.add_to(m)
# ----------------- Get Closest Comm ----------------- #
comm_gdf = get_comms()
closest_comm = comm_gdf.loc[comm_gdf.distance(user_time_location.geometry.iloc[0]).idxmin(), 'distitle']
# ----------------- Finalize Map ----------------- #
folium.TileLayer('CartoDB positron', name='Minimal Base Map').add_to(m)
folium.LayerControl().add_to(m)
# fit bounds to 1 mile buffer of user location
m.fit_bounds([ [user_time_location['lat'].iloc[0] - 0.01, user_time_location['lon'].iloc[0] - 0.01],
[user_time_location['lat'].iloc[0] + 0.01, user_time_location['lon'].iloc[0] + 0.01] ])
# save map to templates/map.html
try:
m.save('templates/map.html')
except:
pass
try:
m.save('/Users/coltonlapp/Dropbox/My Mac (Coltons-MacBook-Pro.local)/Desktop/SCHOOL/Year2_Spring/Spatial DataScience/NearestPointOfInterest/code/templates/map.html')
except:
pass
# create dict to return all info
out_dict = {'map': m,
'random_time': user_time_location['datetime'].iloc[0].strftime('%I:%M %p'),
'random_date': user_time_location['datetime'].iloc[0].strftime('%m/%d/%Y'),
'random_coords': "(" +str( round(user_time_location['lat'].iloc[0], 4)) + ', ' + str(round(user_time_location['lon'].iloc[0], 4)) + ")",
'closest_event_location' : closest_event['name'] if found_event else None,
'closest_event_name': closest_event['eventtitle'] if found_event else None,
'closest_event_desc' : closest_event['eventdescription'] if found_event else None,
'closest_event_time' : closest_event['event_start'].strftime('%I:%M %p') if found_event else None,
'closest_event_date' : closest_event['event_start'].strftime('%m/%d/%Y') if found_event else None,
'closest_event': closest_event if found_event else None,
'directions_result': directions_result if found_event else None,
'directions_html': directions_result['directions_html'] if found_event else [None, None],
'closest_comm': closest_comm}
return out_dict
if __name__ == 'main':
create_map()
print('Map created successfully!')
create_map()