Spaces:
Running
Running
Update create_map_poster.py
Browse files- create_map_poster.py +56 -62
create_map_poster.py
CHANGED
|
@@ -16,9 +16,9 @@ FONTS_DIR = "fonts"
|
|
| 16 |
POSTERS_DIR = "posters"
|
| 17 |
|
| 18 |
# ---- Layout tuning (easy knobs) ----
|
| 19 |
-
FIGSIZE = (12, 16)
|
| 20 |
-
|
| 21 |
-
|
| 22 |
|
| 23 |
def load_fonts():
|
| 24 |
"""
|
|
@@ -183,20 +183,15 @@ def create_gradient_fade(ax, color, location='bottom', zorder=10):
|
|
| 183 |
|
| 184 |
def get_edge_colors_by_type(G):
|
| 185 |
"""
|
| 186 |
-
|
| 187 |
-
Returns a list of colors corresponding to each edge in the graph.
|
| 188 |
"""
|
| 189 |
edge_colors = []
|
| 190 |
|
| 191 |
-
for
|
| 192 |
-
# Get the highway type (can be a list or string)
|
| 193 |
highway = data.get('highway', 'unclassified')
|
| 194 |
-
|
| 195 |
-
# Handle list of highway types (take the first one)
|
| 196 |
if isinstance(highway, list):
|
| 197 |
highway = highway[0] if highway else 'unclassified'
|
| 198 |
|
| 199 |
-
# Assign color based on road type
|
| 200 |
if highway in ['motorway', 'motorway_link']:
|
| 201 |
color = THEME['road_motorway']
|
| 202 |
elif highway in ['trunk', 'trunk_link', 'primary', 'primary_link']:
|
|
@@ -216,18 +211,15 @@ def get_edge_colors_by_type(G):
|
|
| 216 |
|
| 217 |
def get_edge_widths_by_type(G):
|
| 218 |
"""
|
| 219 |
-
|
| 220 |
-
Major roads get thicker lines.
|
| 221 |
"""
|
| 222 |
edge_widths = []
|
| 223 |
|
| 224 |
-
for
|
| 225 |
highway = data.get('highway', 'unclassified')
|
| 226 |
-
|
| 227 |
if isinstance(highway, list):
|
| 228 |
highway = highway[0] if highway else 'unclassified'
|
| 229 |
|
| 230 |
-
# Assign width based on road importance
|
| 231 |
if highway in ['motorway', 'motorway_link']:
|
| 232 |
width = 1.2
|
| 233 |
elif highway in ['trunk', 'trunk_link', 'primary', 'primary_link']:
|
|
@@ -245,13 +237,10 @@ def get_edge_widths_by_type(G):
|
|
| 245 |
|
| 246 |
def get_coordinates(city, country):
|
| 247 |
"""
|
| 248 |
-
|
| 249 |
-
Includes rate limiting to be respectful to the geocoding service.
|
| 250 |
"""
|
| 251 |
print("Looking up coordinates...")
|
| 252 |
geolocator = Nominatim(user_agent="city_map_poster")
|
| 253 |
-
|
| 254 |
-
# Add a small delay to respect Nominatim's usage policy
|
| 255 |
time.sleep(1)
|
| 256 |
|
| 257 |
location = geolocator.geocode(f"{city}, {country}")
|
|
@@ -266,18 +255,22 @@ def get_coordinates(city, country):
|
|
| 266 |
def create_poster(city, country, point, dist, output_file):
|
| 267 |
print(f"\nGenerating map for {city}, {country}...")
|
| 268 |
|
|
|
|
|
|
|
| 269 |
# Progress bar for data fetching
|
| 270 |
-
with tqdm(total=3, desc="Fetching map data", unit="step",
|
|
|
|
|
|
|
| 271 |
# 1. Fetch Street Network
|
| 272 |
pbar.set_description("Downloading street network")
|
| 273 |
-
G = ox.graph_from_point(point, dist=
|
| 274 |
pbar.update(1)
|
| 275 |
-
time.sleep(0.5)
|
| 276 |
|
| 277 |
# 2. Fetch Water Features
|
| 278 |
pbar.set_description("Downloading water features")
|
| 279 |
try:
|
| 280 |
-
water = ox.features_from_point(point, tags={'natural': 'water', 'waterway': 'riverbank'}, dist=
|
| 281 |
except:
|
| 282 |
water = None
|
| 283 |
pbar.update(1)
|
|
@@ -286,7 +279,7 @@ def create_poster(city, country, point, dist, output_file):
|
|
| 286 |
# 3. Fetch Parks
|
| 287 |
pbar.set_description("Downloading parks/green spaces")
|
| 288 |
try:
|
| 289 |
-
parks = ox.features_from_point(point, tags={'leisure': 'park', 'landuse': 'grass'}, dist=
|
| 290 |
except:
|
| 291 |
parks = None
|
| 292 |
pbar.update(1)
|
|
@@ -309,21 +302,25 @@ def create_poster(city, country, point, dist, output_file):
|
|
| 309 |
except:
|
| 310 |
pass
|
| 311 |
|
| 312 |
-
#
|
| 313 |
print("Rendering map...")
|
| 314 |
-
fig
|
| 315 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
|
| 317 |
-
#
|
| 318 |
-
|
| 319 |
-
|
| 320 |
|
| 321 |
-
#
|
| 322 |
# Layer 1: Polygons
|
| 323 |
if water is not None and not getattr(water, "empty", True):
|
| 324 |
-
water.plot(ax=
|
| 325 |
if parks is not None and not getattr(parks, "empty", True):
|
| 326 |
-
parks.plot(ax=
|
| 327 |
|
| 328 |
# Layer 2: Roads with hierarchy coloring
|
| 329 |
print("Applying road hierarchy colors...")
|
|
@@ -331,64 +328,59 @@ def create_poster(city, country, point, dist, output_file):
|
|
| 331 |
edge_widths = get_edge_widths_by_type(G)
|
| 332 |
|
| 333 |
ox.plot_graph(
|
| 334 |
-
G, ax=
|
| 335 |
node_size=0,
|
| 336 |
edge_color=edge_colors,
|
| 337 |
edge_linewidth=edge_widths,
|
| 338 |
show=False, close=False
|
| 339 |
)
|
| 340 |
|
| 341 |
-
# Crop view to match
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
crop_axes_to_ratio(ax, target_ratio=target_ratio, pad=MAP_PAD)
|
| 345 |
|
| 346 |
# Layer 3: Gradients (Top and Bottom)
|
| 347 |
-
create_gradient_fade(
|
| 348 |
-
create_gradient_fade(
|
| 349 |
|
| 350 |
-
#
|
| 351 |
if FONTS:
|
| 352 |
font_main = FontProperties(fname=FONTS['bold'], size=60)
|
| 353 |
font_sub = FontProperties(fname=FONTS['light'], size=22)
|
| 354 |
font_coords = FontProperties(fname=FONTS['regular'], size=14)
|
|
|
|
| 355 |
else:
|
| 356 |
-
# Fallback to system fonts
|
| 357 |
font_main = FontProperties(family='monospace', weight='bold', size=60)
|
| 358 |
font_sub = FontProperties(family='monospace', weight='normal', size=22)
|
| 359 |
font_coords = FontProperties(family='monospace', size=14)
|
|
|
|
| 360 |
|
| 361 |
spaced_city = " ".join(list(city.upper()))
|
| 362 |
|
| 363 |
-
# --- BOTTOM TEXT ---
|
| 364 |
-
|
| 365 |
-
|
| 366 |
|
| 367 |
-
|
| 368 |
-
|
| 369 |
|
| 370 |
lat, lon = point
|
| 371 |
coords = f"{lat:.4f}° N / {lon:.4f}° E" if lat >= 0 else f"{abs(lat):.4f}° S / {lon:.4f}° E"
|
| 372 |
if lon < 0:
|
| 373 |
coords = coords.replace("E", "W")
|
| 374 |
|
| 375 |
-
|
| 376 |
-
|
| 377 |
|
| 378 |
-
|
| 379 |
-
|
| 380 |
|
| 381 |
# --- ATTRIBUTION (bottom right) ---
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
font_attr = FontProperties(family='monospace', size=8)
|
| 386 |
-
|
| 387 |
-
ax.text(0.98, 0.02, "© OpenStreetMap contributors", transform=ax.transAxes,
|
| 388 |
-
color=THEME['text'], alpha=0.5, ha='right', va='bottom',
|
| 389 |
-
fontproperties=font_attr, zorder=11)
|
| 390 |
|
| 391 |
-
#
|
| 392 |
print(f"Saving to {output_file}...")
|
| 393 |
plt.savefig(output_file, dpi=300, facecolor=THEME['bg'])
|
| 394 |
plt.close()
|
|
@@ -490,8 +482,10 @@ Examples:
|
|
| 490 |
|
| 491 |
parser.add_argument('--city', '-c', type=str, help='City name')
|
| 492 |
parser.add_argument('--country', '-C', type=str, help='Country name')
|
| 493 |
-
parser.add_argument('--theme', '-t', type=str, default='feature_based',
|
| 494 |
-
|
|
|
|
|
|
|
| 495 |
parser.add_argument('--list-themes', action='store_true', help='List all available themes')
|
| 496 |
|
| 497 |
args = parser.parse_args()
|
|
|
|
| 16 |
POSTERS_DIR = "posters"
|
| 17 |
|
| 18 |
# ---- Layout tuning (easy knobs) ----
|
| 19 |
+
FIGSIZE = (12, 16) # Poster size in inches (width, height)
|
| 20 |
+
MAP_PAD = 0.03 # Crop padding fraction (3% margin)
|
| 21 |
+
OVERFETCH = 1.20 # Fetch a bit larger area, then crop to poster ratio
|
| 22 |
|
| 23 |
def load_fonts():
|
| 24 |
"""
|
|
|
|
| 183 |
|
| 184 |
def get_edge_colors_by_type(G):
|
| 185 |
"""
|
| 186 |
+
Assign colors to edges based on road type hierarchy.
|
|
|
|
| 187 |
"""
|
| 188 |
edge_colors = []
|
| 189 |
|
| 190 |
+
for _, _, data in G.edges(data=True):
|
|
|
|
| 191 |
highway = data.get('highway', 'unclassified')
|
|
|
|
|
|
|
| 192 |
if isinstance(highway, list):
|
| 193 |
highway = highway[0] if highway else 'unclassified'
|
| 194 |
|
|
|
|
| 195 |
if highway in ['motorway', 'motorway_link']:
|
| 196 |
color = THEME['road_motorway']
|
| 197 |
elif highway in ['trunk', 'trunk_link', 'primary', 'primary_link']:
|
|
|
|
| 211 |
|
| 212 |
def get_edge_widths_by_type(G):
|
| 213 |
"""
|
| 214 |
+
Assign line widths to edges based on road type.
|
|
|
|
| 215 |
"""
|
| 216 |
edge_widths = []
|
| 217 |
|
| 218 |
+
for _, _, data in G.edges(data=True):
|
| 219 |
highway = data.get('highway', 'unclassified')
|
|
|
|
| 220 |
if isinstance(highway, list):
|
| 221 |
highway = highway[0] if highway else 'unclassified'
|
| 222 |
|
|
|
|
| 223 |
if highway in ['motorway', 'motorway_link']:
|
| 224 |
width = 1.2
|
| 225 |
elif highway in ['trunk', 'trunk_link', 'primary', 'primary_link']:
|
|
|
|
| 237 |
|
| 238 |
def get_coordinates(city, country):
|
| 239 |
"""
|
| 240 |
+
Fetch coordinates for a given city and country using geopy.
|
|
|
|
| 241 |
"""
|
| 242 |
print("Looking up coordinates...")
|
| 243 |
geolocator = Nominatim(user_agent="city_map_poster")
|
|
|
|
|
|
|
| 244 |
time.sleep(1)
|
| 245 |
|
| 246 |
location = geolocator.geocode(f"{city}, {country}")
|
|
|
|
| 255 |
def create_poster(city, country, point, dist, output_file):
|
| 256 |
print(f"\nGenerating map for {city}, {country}...")
|
| 257 |
|
| 258 |
+
dist_fetch = int(dist * OVERFETCH)
|
| 259 |
+
|
| 260 |
# Progress bar for data fetching
|
| 261 |
+
with tqdm(total=3, desc="Fetching map data", unit="step",
|
| 262 |
+
bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt}') as pbar:
|
| 263 |
+
|
| 264 |
# 1. Fetch Street Network
|
| 265 |
pbar.set_description("Downloading street network")
|
| 266 |
+
G = ox.graph_from_point(point, dist=dist_fetch, dist_type='bbox', network_type='all')
|
| 267 |
pbar.update(1)
|
| 268 |
+
time.sleep(0.5)
|
| 269 |
|
| 270 |
# 2. Fetch Water Features
|
| 271 |
pbar.set_description("Downloading water features")
|
| 272 |
try:
|
| 273 |
+
water = ox.features_from_point(point, tags={'natural': 'water', 'waterway': 'riverbank'}, dist=dist_fetch)
|
| 274 |
except:
|
| 275 |
water = None
|
| 276 |
pbar.update(1)
|
|
|
|
| 279 |
# 3. Fetch Parks
|
| 280 |
pbar.set_description("Downloading parks/green spaces")
|
| 281 |
try:
|
| 282 |
+
parks = ox.features_from_point(point, tags={'leisure': 'park', 'landuse': 'grass'}, dist=dist_fetch)
|
| 283 |
except:
|
| 284 |
parks = None
|
| 285 |
pbar.update(1)
|
|
|
|
| 302 |
except:
|
| 303 |
pass
|
| 304 |
|
| 305 |
+
# --- Setup Plot (two axes: map full-bleed + typography overlay) ---
|
| 306 |
print("Rendering map...")
|
| 307 |
+
fig = plt.figure(figsize=FIGSIZE, facecolor=THEME['bg'])
|
| 308 |
+
|
| 309 |
+
# Map axis: full-bleed
|
| 310 |
+
ax_map = fig.add_axes([0, 0, 1, 1])
|
| 311 |
+
ax_map.set_facecolor(THEME['bg'])
|
| 312 |
+
ax_map.set_axis_off()
|
| 313 |
|
| 314 |
+
# Typography axis: overlay, full-bleed
|
| 315 |
+
ax_page = fig.add_axes([0, 0, 1, 1], facecolor="none")
|
| 316 |
+
ax_page.set_axis_off()
|
| 317 |
|
| 318 |
+
# --- Plot Layers ---
|
| 319 |
# Layer 1: Polygons
|
| 320 |
if water is not None and not getattr(water, "empty", True):
|
| 321 |
+
water.plot(ax=ax_map, facecolor=THEME['water'], edgecolor='none', zorder=1)
|
| 322 |
if parks is not None and not getattr(parks, "empty", True):
|
| 323 |
+
parks.plot(ax=ax_map, facecolor=THEME['parks'], edgecolor='none', zorder=2)
|
| 324 |
|
| 325 |
# Layer 2: Roads with hierarchy coloring
|
| 326 |
print("Applying road hierarchy colors...")
|
|
|
|
| 328 |
edge_widths = get_edge_widths_by_type(G)
|
| 329 |
|
| 330 |
ox.plot_graph(
|
| 331 |
+
G, ax=ax_map, bgcolor=THEME['bg'],
|
| 332 |
node_size=0,
|
| 333 |
edge_color=edge_colors,
|
| 334 |
edge_linewidth=edge_widths,
|
| 335 |
show=False, close=False
|
| 336 |
)
|
| 337 |
|
| 338 |
+
# Crop view to match poster aspect ratio (full-bleed)
|
| 339 |
+
target_ratio = FIGSIZE[0] / FIGSIZE[1] # width/height
|
| 340 |
+
crop_axes_to_ratio(ax_map, target_ratio=target_ratio, pad=MAP_PAD)
|
|
|
|
| 341 |
|
| 342 |
# Layer 3: Gradients (Top and Bottom)
|
| 343 |
+
create_gradient_fade(ax_map, THEME['gradient_color'], location='bottom', zorder=10)
|
| 344 |
+
create_gradient_fade(ax_map, THEME['gradient_color'], location='top', zorder=10)
|
| 345 |
|
| 346 |
+
# --- Typography using Roboto font ---
|
| 347 |
if FONTS:
|
| 348 |
font_main = FontProperties(fname=FONTS['bold'], size=60)
|
| 349 |
font_sub = FontProperties(fname=FONTS['light'], size=22)
|
| 350 |
font_coords = FontProperties(fname=FONTS['regular'], size=14)
|
| 351 |
+
font_attr = FontProperties(fname=FONTS['light'], size=8)
|
| 352 |
else:
|
|
|
|
| 353 |
font_main = FontProperties(family='monospace', weight='bold', size=60)
|
| 354 |
font_sub = FontProperties(family='monospace', weight='normal', size=22)
|
| 355 |
font_coords = FontProperties(family='monospace', size=14)
|
| 356 |
+
font_attr = FontProperties(family='monospace', size=8)
|
| 357 |
|
| 358 |
spaced_city = " ".join(list(city.upper()))
|
| 359 |
|
| 360 |
+
# --- BOTTOM TEXT (page coordinates) ---
|
| 361 |
+
ax_page.text(0.5, 0.14, spaced_city, transform=ax_page.transAxes,
|
| 362 |
+
color=THEME['text'], ha='center', fontproperties=font_main, zorder=11)
|
| 363 |
|
| 364 |
+
ax_page.text(0.5, 0.10, country.upper(), transform=ax_page.transAxes,
|
| 365 |
+
color=THEME['text'], ha='center', fontproperties=font_sub, zorder=11)
|
| 366 |
|
| 367 |
lat, lon = point
|
| 368 |
coords = f"{lat:.4f}° N / {lon:.4f}° E" if lat >= 0 else f"{abs(lat):.4f}° S / {lon:.4f}° E"
|
| 369 |
if lon < 0:
|
| 370 |
coords = coords.replace("E", "W")
|
| 371 |
|
| 372 |
+
ax_page.text(0.5, 0.07, coords, transform=ax_page.transAxes,
|
| 373 |
+
color=THEME['text'], alpha=0.7, ha='center', fontproperties=font_coords, zorder=11)
|
| 374 |
|
| 375 |
+
ax_page.plot([0.4, 0.6], [0.125, 0.125], transform=ax_page.transAxes,
|
| 376 |
+
color=THEME['text'], linewidth=1, zorder=11)
|
| 377 |
|
| 378 |
# --- ATTRIBUTION (bottom right) ---
|
| 379 |
+
ax_page.text(0.98, 0.02, "© OpenStreetMap contributors", transform=ax_page.transAxes,
|
| 380 |
+
color=THEME['text'], alpha=0.5, ha='right', va='bottom',
|
| 381 |
+
fontproperties=font_attr, zorder=11)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
|
| 383 |
+
# --- Save ---
|
| 384 |
print(f"Saving to {output_file}...")
|
| 385 |
plt.savefig(output_file, dpi=300, facecolor=THEME['bg'])
|
| 386 |
plt.close()
|
|
|
|
| 482 |
|
| 483 |
parser.add_argument('--city', '-c', type=str, help='City name')
|
| 484 |
parser.add_argument('--country', '-C', type=str, help='Country name')
|
| 485 |
+
parser.add_argument('--theme', '-t', type=str, default='feature_based',
|
| 486 |
+
help='Theme name (default: feature_based)')
|
| 487 |
+
parser.add_argument('--distance', '-d', type=int, default=29000,
|
| 488 |
+
help='Map radius in meters (default: 29000)')
|
| 489 |
parser.add_argument('--list-themes', action='store_true', help='List all available themes')
|
| 490 |
|
| 491 |
args = parser.parse_args()
|