thadillo Claude commited on
Commit
abc6061
·
1 Parent(s): 0d1a6bf

Add real OpenStreetMap tiles to PDF export

Browse files

Features:
- Use contextily library to add real OSM basemap tiles
- Map now shows actual streets, buildings, and geography
- Larger, higher resolution map (7x5.5 inches, 200 DPI)
- Improved marker styling (white borders, larger size)
- Legend positioned outside plot area
- OSM attribution added
- Graceful fallback to grid if tiles unavailable

Map now matches dashboard Leaflet map appearance

Dependencies added:
- contextily>=1.3.0 (for OpenStreetMap tiles)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (2) hide show
  1. app/utils/pdf_export.py +41 -11
  2. requirements.txt +1 -0
app/utils/pdf_export.py CHANGED
@@ -14,6 +14,11 @@ import matplotlib
14
  matplotlib.use('Agg') # Use non-interactive backend
15
  import matplotlib.pyplot as plt
16
  import numpy as np
 
 
 
 
 
17
 
18
 
19
  class DashboardPDFExporter:
@@ -255,7 +260,7 @@ class DashboardPDFExporter:
255
  return table
256
 
257
  def _create_map(self, geotagged_submissions, categories):
258
- """Create geographic distribution map using matplotlib"""
259
  if not geotagged_submissions:
260
  return None
261
 
@@ -266,7 +271,7 @@ class DashboardPDFExporter:
266
  cats = [s.category for s in geotagged_submissions]
267
 
268
  # Create matplotlib figure
269
- fig, ax = plt.subplots(figsize=(8, 6))
270
 
271
  # Color map for categories
272
  category_colors = {
@@ -283,24 +288,49 @@ class DashboardPDFExporter:
283
  cat_lats = [lat for lat, cat in zip(lats, cats) if cat == category]
284
  cat_lons = [lon for lon, cat in zip(lons, cats) if cat == category]
285
  color = category_colors.get(category, '#95a5a6')
286
- ax.scatter(cat_lons, cat_lats, c=color, label=category, s=100, alpha=0.7, edgecolors='black')
287
-
288
- ax.set_xlabel('Longitude', fontsize=11, fontweight='bold')
289
- ax.set_ylabel('Latitude', fontsize=11, fontweight='bold')
290
- ax.set_title('Geographic Distribution of Submissions', fontsize=14, fontweight='bold')
291
- ax.legend(loc='best', fontsize=9)
292
- ax.grid(True, alpha=0.3)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
 
294
  # Convert to image
295
  img_buffer = io.BytesIO()
296
  plt.tight_layout()
297
- plt.savefig(img_buffer, format='png', dpi=150, bbox_inches='tight')
298
  plt.close(fig)
299
  img_buffer.seek(0)
300
 
301
- img = Image(img_buffer, width=6*inch, height=4.5*inch)
302
  return img
303
 
304
  except Exception as e:
305
  print(f"Error creating map: {e}")
 
 
306
  return None
 
14
  matplotlib.use('Agg') # Use non-interactive backend
15
  import matplotlib.pyplot as plt
16
  import numpy as np
17
+ try:
18
+ import contextily as cx
19
+ HAS_CONTEXTILY = True
20
+ except ImportError:
21
+ HAS_CONTEXTILY = False
22
 
23
 
24
  class DashboardPDFExporter:
 
260
  return table
261
 
262
  def _create_map(self, geotagged_submissions, categories):
263
+ """Create geographic distribution map with real OpenStreetMap tiles"""
264
  if not geotagged_submissions:
265
  return None
266
 
 
271
  cats = [s.category for s in geotagged_submissions]
272
 
273
  # Create matplotlib figure
274
+ fig, ax = plt.subplots(figsize=(10, 8))
275
 
276
  # Color map for categories
277
  category_colors = {
 
288
  cat_lats = [lat for lat, cat in zip(lats, cats) if cat == category]
289
  cat_lons = [lon for lon, cat in zip(lons, cats) if cat == category]
290
  color = category_colors.get(category, '#95a5a6')
291
+ ax.scatter(cat_lons, cat_lats, c=color, label=category,
292
+ s=150, alpha=0.8, edgecolors='white', linewidths=2, zorder=5)
293
+
294
+ # Add OpenStreetMap basemap if contextily is available
295
+ if HAS_CONTEXTILY:
296
+ try:
297
+ # Add map tiles
298
+ cx.add_basemap(ax, crs='EPSG:4326', source=cx.providers.OpenStreetMap.Mapnik,
299
+ attribution=False, alpha=0.8)
300
+ except Exception as e:
301
+ print(f"Could not add basemap: {e}")
302
+ # Fallback to grid
303
+ ax.grid(True, alpha=0.3)
304
+ else:
305
+ # Fallback: simple grid
306
+ ax.grid(True, alpha=0.3)
307
+
308
+ ax.set_xlabel('Longitude', fontsize=12, fontweight='bold')
309
+ ax.set_ylabel('Latitude', fontsize=12, fontweight='bold')
310
+ ax.set_title('Geographic Distribution of Submissions',
311
+ fontsize=16, fontweight='bold', pad=20)
312
+
313
+ # Legend outside plot area
314
+ ax.legend(loc='upper left', bbox_to_anchor=(1.02, 1),
315
+ fontsize=10, frameon=True, fancybox=True, shadow=True)
316
+
317
+ # Add attribution text if using OpenStreetMap
318
+ if HAS_CONTEXTILY:
319
+ fig.text(0.99, 0.01, '© OpenStreetMap contributors',
320
+ ha='right', va='bottom', fontsize=7, style='italic', alpha=0.7)
321
 
322
  # Convert to image
323
  img_buffer = io.BytesIO()
324
  plt.tight_layout()
325
+ plt.savefig(img_buffer, format='png', dpi=200, bbox_inches='tight')
326
  plt.close(fig)
327
  img_buffer.seek(0)
328
 
329
+ img = Image(img_buffer, width=7*inch, height=5.5*inch)
330
  return img
331
 
332
  except Exception as e:
333
  print(f"Error creating map: {e}")
334
+ import traceback
335
+ traceback.print_exc()
336
  return None
requirements.txt CHANGED
@@ -22,3 +22,4 @@ nltk>=3.8.0
22
 
23
  # PDF generation
24
  reportlab>=4.0.0
 
 
22
 
23
  # PDF generation
24
  reportlab>=4.0.0
25
+ contextily>=1.3.0