fffiloni commited on
Commit
18c2ef3
·
verified ·
1 Parent(s): 34b0ee2

Update create_map_poster.py

Browse files
Files changed (1) hide show
  1. 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) # Poster size in inches
20
- MAP_POS = (0.03, 0.18, 0.94, 0.80) # [left, bottom, width, height] in figure coords
21
- MAP_PAD = 0.03 # Crop padding fraction (3% margin)
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
- Assigns colors to edges based on road type hierarchy.
187
- Returns a list of colors corresponding to each edge in the graph.
188
  """
189
  edge_colors = []
190
 
191
- for u, v, data in G.edges(data=True):
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
- Assigns line widths to edges based on road type.
220
- Major roads get thicker lines.
221
  """
222
  edge_widths = []
223
 
224
- for u, v, data in G.edges(data=True):
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
- Fetches coordinates for a given city and country using geopy.
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", bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt}') as pbar:
 
 
271
  # 1. Fetch Street Network
272
  pbar.set_description("Downloading street network")
273
- G = ox.graph_from_point(point, dist=dist, dist_type='bbox', network_type='all')
274
  pbar.update(1)
275
- time.sleep(0.5) # Rate limit between requests
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=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=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
- # 2. Setup Plot
313
  print("Rendering map...")
314
- fig, ax = plt.subplots(figsize=FIGSIZE, facecolor=THEME['bg'])
315
- ax.set_facecolor(THEME['bg'])
 
 
 
 
316
 
317
- # Reserve a dedicated map area (leave room for bottom typography)
318
- ax.set_position(list(MAP_POS))
319
- map_h_frac = MAP_POS[3]
320
 
321
- # 3. Plot Layers
322
  # Layer 1: Polygons
323
  if water is not None and not getattr(water, "empty", True):
324
- water.plot(ax=ax, facecolor=THEME['water'], edgecolor='none', zorder=1)
325
  if parks is not None and not getattr(parks, "empty", True):
326
- parks.plot(ax=ax, facecolor=THEME['parks'], edgecolor='none', zorder=2)
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=ax, bgcolor=THEME['bg'],
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 the poster/map area aspect ratio (in projected units)
342
- fig_w, fig_h = fig.get_size_inches()
343
- target_ratio = fig_w / (fig_h * map_h_frac) # width/height of the map area
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(ax, THEME['gradient_color'], location='bottom', zorder=10)
348
- create_gradient_fade(ax, THEME['gradient_color'], location='top', zorder=10)
349
 
350
- # 4. Typography using Roboto font
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
- ax.text(0.5, 0.14, spaced_city, transform=ax.transAxes,
365
- color=THEME['text'], ha='center', fontproperties=font_main, zorder=11)
366
 
367
- ax.text(0.5, 0.10, country.upper(), transform=ax.transAxes,
368
- color=THEME['text'], ha='center', fontproperties=font_sub, zorder=11)
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
- ax.text(0.5, 0.07, coords, transform=ax.transAxes,
376
- color=THEME['text'], alpha=0.7, ha='center', fontproperties=font_coords, zorder=11)
377
 
378
- ax.plot([0.4, 0.6], [0.125, 0.125], transform=ax.transAxes,
379
- color=THEME['text'], linewidth=1, zorder=11)
380
 
381
  # --- ATTRIBUTION (bottom right) ---
382
- if FONTS:
383
- font_attr = FontProperties(fname=FONTS['light'], size=8)
384
- else:
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
- # 5. Save
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', help='Theme name (default: feature_based)')
494
- parser.add_argument('--distance', '-d', type=int, default=29000, help='Map radius in meters (default: 29000)')
 
 
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()