Budapest Isochrones — isochrones show areas that are reachable within a certain amount of time. Here, I highlight the isochrones (overlapped with the road network) of Budapest, focusing on the driving road network and the simplification of no traffic — each color shad corresponds to different reachability ranges from the city center, increasing by the minute.
Imports and tagret area
# import osmnx
import osmnx as ox
import numpy as np
import os
import networkx as nx
from shapely.geometry import Point, Polygon
import geopandas as gpd
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.colors as mcolors
folderout = 'frames'
if not os.path.exists(folderout):
os.makedirs(folderout)
city = 'Budapest, Hungary'
admin = ox.geocode_to_gdf(city)
admin.plot()
Getting the Graph
# Download the road network
G = ox.graph_from_polygon(admin.geometry.to_list()[0], network_type = 'drive')
print('Number of intersections: ', G.number_of_nodes())
print('Number of road segments: ', G.number_of_edges())
# turning the graph into geodataframes
nodes, edges = ox.graph_to_gdfs(G)
Isochrones
# Define the walking speed (5 km/h -> 1.39 m/s)
walking_speed = 11.12 # in meters per second
# Calculate travel time for each edge
for u, v, data in G.edges(data=True):
# Calculate travel time in seconds
data['travel_time'] = data['length'] / walking_speed
# Pick a center node
center_node = 251280825 # starting point
# Generate isochrones
isochrone_times = np.arange(1,46,1) # isochrones in minutes
len(isochrone_times)
isochrone_polys = []
for time in isochrone_times:
subgraph = nx.ego_graph(G,
center_node,
radius=time*60,
distance='travel_time')
node_points = [Point((data['x'], data['y'])) \
for node, data in subgraph.nodes(data=True)]
polygon = Polygon(gpd.GeoSeries(node_points).unary_union.convex_hull)
isochrone_polys.append(gpd.GeoSeries([polygon]))
len(isochrone_polys)
Visuals
color = 'grey'
color_bcg = 'k'
width = 1.5
f, ax = plt.subplots(1,1,figsize=(12,12))
edges.plot(ax = ax, color = color, linewidth = width, alpha = 0.9)
ax.set_facecolor(color_bcg)
# get rid of the ticks
for xlabel_i in ax.get_xticklabels(): xlabel_i.set_visible(False)
for ylabel_i in ax.get_yticklabels(): ylabel_i.set_visible(False)
for tick in ax.get_xticklines(): tick.set_visible(False)
for tick in ax.get_yticklines(): tick.set_visible(False)
# add the title
city = 'Budapest'
ymin, ymax = plt.ylim()
extension = 0.1 * (ymax - ymin)
ax.set_ylim(ymin, ymax + extension)
ax.set_title(city, fontsize = 40, color = 'white', y = 0.95)
plt.tight_layout()
plt.savefig(folderout + '/' + '0.png', dpi = 200, bbox_inches = 'tight')
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import numpy as np
import warnings
warnings.filterwarnings('ignore')
# Define the colormap
cmap = mcolors.LinearSegmentedColormap.from_list("blue_pink", ["#4DFFFD", "#ff6ec7"])
for idx, (polygon, time) in \
enumerate(list(zip(isochrone_polys, isochrone_times))):
if idx < 70:
# Create figure and axis
f, ax = plt.subplots(1,1,figsize=(12,12))
edges.plot(ax = ax, color = 'grey', linewidth = width, alpha = 0.3)
edges.plot(ax = ax, color = color, linewidth = width-0.5, alpha = 0.97)
edges_temp = gpd.overlay(edges, gpd.GeoDataFrame(polygon, columns = ['geometry']))
edges_temp.plot(ax=ax, color=cmap(idx / len(isochrone_times)), alpha=0.96, label=f'{time} min')
ax.set_facecolor(color_bcg)
# get rid of the ticks
for xlabel_i in ax.get_xticklabels(): xlabel_i.set_visible(False)
for ylabel_i in ax.get_yticklabels(): ylabel_i.set_visible(False)
for tick in ax.get_xticklines(): tick.set_visible(False)
for tick in ax.get_yticklines(): tick.set_visible(False)
# add the title
city = 'Budapest - ' + str(isochrone_times[idx]) + ' minutes'
ymin, ymax = plt.ylim()
extension = 0.1 * (ymax - ymin)
ax.set_ylim(ymin, ymax + extension)
ax.set_title(city, fontsize = 40, color = 'white', y = 0.95)
#ax.plot([0], [i], marker='o', markersize=15, color=cmap(i / len(isochrone_times)))
plt.tight_layout()
plt.savefig(folderout + '/' + str(idx+1) + '.png', dpi = 200, bbox_inches = 'tight')
from PIL import Image, ImageDraw
png_files = [str(i) + '.png' for i in isochrone_times]
frames = []
for png_file in png_files:
file_path = os.path.join(folderout, png_file)
img = Image.open(file_path)
frames.append(img)
# Define the duration for each frame in milliseconds
frame_duration = 18
# Define the number of frames for the crossfade transition
crossfade_frames = 10
# Create a list to store the crossfaded frames
crossfaded_frames = []
# Iterate through pairs of consecutive frames
for i in range(len(frames) - 1):
# Extract current and next frames
current_frame = frames[i]
next_frame = frames[i + 1]
# Create a sequence of crossfaded frames between the current and next frames
for j in range(crossfade_frames + 1):
# Calculate the alpha value for blending
alpha = j / crossfade_frames
# Blend the current and next frames using alpha blending
blended_frame = Image.blend(current_frame, next_frame, alpha)
# Append the blended frame to the list of crossfaded frames
crossfaded_frames.append(blended_frame)
# Add the last frame without crossfading
crossfaded_frames.append(frames[-1])
output_gif_path = 'footage_complete.gif'
# Save the GIF with crossfaded frames
crossfaded_frames[0].save(
output_gif_path,
save_all=True,
append_images=crossfaded_frames[1:],
duration=frame_duration, # Set the duration between frames in milliseconds
loop=0 # Set loop to 0 for an infinite loop, or any positive integer for a finite loop
)
Share this post