0:00
/
0:00

Budapest Isochrones

#30DayMapChallenge - Day 2 - Lines

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
)

Discussion about this video

User's avatar