Spaces:
Sleeping
Sleeping
thadillo
Claude
commited on
Commit
·
abc6061
1
Parent(s):
0d1a6bf
Add real OpenStreetMap tiles to PDF export
Browse filesFeatures:
- 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>
- app/utils/pdf_export.py +41 -11
- 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
|
| 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=(
|
| 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,
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
|
| 294 |
# Convert to image
|
| 295 |
img_buffer = io.BytesIO()
|
| 296 |
plt.tight_layout()
|
| 297 |
-
plt.savefig(img_buffer, format='png', dpi=
|
| 298 |
plt.close(fig)
|
| 299 |
img_buffer.seek(0)
|
| 300 |
|
| 301 |
-
img = Image(img_buffer, width=
|
| 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
|