Spaces:
Running
on
Zero
Running
on
Zero
import gradio as gr | |
import pandas as pd | |
import folium | |
from folium.plugins import MeasureControl, Fullscreen, Search | |
from geopy.geocoders import Nominatim | |
import tempfile | |
import warnings | |
import os | |
import time | |
import random | |
from datetime import datetime | |
warnings.filterwarnings("ignore") | |
# Updated Historical Tile Providers with reliable sources | |
HISTORICAL_TILES = { | |
"Historical 1700s-1800s": { | |
"url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", | |
"attr": "Esri", | |
"fallback": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}", | |
"years": (1700, 1900) | |
}, | |
"Early 1900s": { | |
"url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}", | |
"attr": "Esri", | |
"fallback": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", | |
"years": (1901, 1920) | |
}, | |
"Modern Era": { | |
"url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", | |
"attr": "OpenStreetMap", | |
"fallback": None, | |
"years": (1921, 2023) | |
}, | |
# Additional reliable tile sources | |
"Terrain": { | |
"url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Terrain_Base/MapServer/tile/{z}/{y}/{x}", | |
"attr": "Esri", | |
"fallback": None, | |
"years": (1700, 2023) | |
}, | |
"Toner": { | |
"url": "https://tiles.stadiamaps.com/tiles/stamen_toner/{z}/{x}/{y}.png", # Updated Stamen source | |
"attr": "Stadia Maps", | |
"fallback": None, | |
"years": (1700, 2023) | |
} | |
} | |
class SafeGeocoder: | |
def __init__(self): | |
user_agent = f"historical_mapper_v7_{random.randint(1000, 9999)}" | |
self.geolocator = Nominatim(user_agent=user_agent, timeout=10) | |
self.cache = {} # Simple cache to avoid repeated requests | |
self.last_request = 0 | |
def _respect_rate_limit(self): | |
# Ensure at least 1 second between requests | |
current_time = time.time() | |
elapsed = current_time - self.last_request | |
if elapsed < 1.0: | |
time.sleep(1.0 - elapsed) | |
self.last_request = time.time() | |
def get_coords(self, location: str): | |
if not location or pd.isna(location): | |
return None | |
# Convert to string if needed | |
location = str(location).strip() | |
# Check cache first | |
if location in self.cache: | |
return self.cache[location] | |
try: | |
self._respect_rate_limit() | |
result = self.geolocator.geocode(location) | |
if result: | |
coords = (result.latitude, result.longitude) | |
self.cache[location] = coords | |
return coords | |
self.cache[location] = None | |
return None | |
except Exception as e: | |
print(f"Geocoding error for '{location}': {e}") | |
self.cache[location] = None | |
return None | |
def create_reliable_map(df, location_col, year): | |
"""Create a map with multiple layer options and better error handling""" | |
# Select appropriate default tile configuration based on year | |
default_tile_name = next( | |
(name for name, t in HISTORICAL_TILES.items() | |
if t["years"][0] <= year <= t["years"][1] and name in ["Historical 1700s-1800s", "Early 1900s", "Modern Era"]), | |
"Modern Era" | |
) | |
# Initialize map | |
m = folium.Map(location=[20, 0], zoom_start=2, control_scale=True) | |
# Add all tile layers with the appropriate one active | |
for name, config in HISTORICAL_TILES.items(): | |
folium.TileLayer( | |
tiles=config["url"], | |
attr=f"{config['attr']} ({name})", | |
name=name, | |
overlay=False, | |
control=True, | |
show=(name == default_tile_name) # Only show the default layer initially | |
).add_to(m) | |
# Add plugins for better user experience | |
Fullscreen().add_to(m) | |
MeasureControl(position='topright', primary_length_unit='kilometers').add_to(m) | |
# Add markers | |
geocoder = SafeGeocoder() | |
coords = [] | |
# Create marker cluster for better performance with many points | |
marker_cluster = folium.MarkerCluster(name="Locations").add_to(m) | |
# Process each location | |
processed_count = 0 | |
for idx, row in df.iterrows(): | |
if pd.isna(row[location_col]): | |
continue | |
location = str(row[location_col]).strip() | |
# Get additional info if available | |
additional_info = "" | |
for col in df.columns: | |
if col != location_col and not pd.isna(row[col]): | |
additional_info += f"<br><b>{col}:</b> {row[col]}" | |
# Geocode location | |
point = geocoder.get_coords(location) | |
if point: | |
# Create popup content | |
popup_content = f""" | |
<div style="min-width: 200px; max-width: 300px"> | |
<h4>{location}</h4> | |
<p><i>Historical View ({year})</i></p> | |
{additional_info} | |
</div> | |
""" | |
# Add marker | |
folium.Marker( | |
location=point, | |
popup=folium.Popup(popup_content, max_width=300), | |
tooltip=location, | |
icon=folium.Icon(color="blue", icon="info-sign") | |
).add_to(marker_cluster) | |
coords.append(point) | |
processed_count += 1 | |
# Layer control | |
folium.LayerControl(collapsed=False).add_to(m) | |
# Set bounds if we have coordinates | |
if coords: | |
m.fit_bounds(coords) | |
# Add better tile error handling with JavaScript | |
m.get_root().html.add_child(folium.Element(""" | |
<script> | |
// Wait for the map to be fully loaded | |
document.addEventListener('DOMContentLoaded', function() { | |
setTimeout(function() { | |
// Get the map instance | |
var maps = document.querySelectorAll('.leaflet-container'); | |
if (maps.length > 0) { | |
var map = maps[0]; | |
// Add error handler for tiles | |
var layers = map.querySelectorAll('.leaflet-tile-pane .leaflet-layer'); | |
for (var i = 0; i < layers.length; i++) { | |
var layer = layers[i]; | |
var tiles = layer.querySelectorAll('.leaflet-tile'); | |
// Check if layer has no loaded tiles | |
var loadedTiles = layer.querySelectorAll('.leaflet-tile-loaded'); | |
if (tiles.length > 0 && loadedTiles.length === 0) { | |
// Force switch to OpenStreetMap if current layer failed | |
var osmButton = document.querySelector('.leaflet-control-layers-list input[type="radio"]:nth-child(3)'); | |
if (osmButton) { | |
osmButton.click(); | |
} | |
console.log("Switched to fallback tile layer due to loading issues"); | |
} | |
} | |
} | |
}, 3000); // Wait 3 seconds for tiles to load | |
}); | |
</script> | |
""")) | |
return m._repr_html_(), processed_count | |
def process_data(file_obj, location_col, year): | |
try: | |
# Handle file reading | |
try: | |
df = pd.read_excel(file_obj.name) | |
except Exception as e: | |
return None, f"Error reading Excel file: {str(e)}", None | |
# Validate columns | |
if location_col not in df.columns: | |
return None, f"Column '{location_col}' not found. Available columns: {', '.join(df.columns)}", None | |
# Create map | |
map_html, processed_count = create_reliable_map(df, location_col, year) | |
# Save processed data | |
with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp: | |
df.to_excel(tmp.name, index=False) | |
processed_path = tmp.name | |
# Generate stats | |
total_locations = df[location_col].count() | |
success_rate = (processed_count / total_locations * 100) if total_locations > 0 else 0 | |
stats = f"Found {processed_count} of {total_locations} locations ({success_rate:.1f}%) from year {year}" | |
return ( | |
f"<div style='width:100%; height:70vh; border:1px solid #ddd'>{map_html}</div>", | |
stats, | |
processed_path | |
) | |
except Exception as e: | |
import traceback | |
error_details = traceback.format_exc() | |
print(f"Error in processing: {error_details}") | |
return None, f"Error: {str(e)}", None | |
# Gradio Interface | |
with gr.Blocks(title="Historical Maps", theme=gr.themes.Soft()) as app: | |
gr.Markdown("# Historical Map Viewer") | |
gr.Markdown("Upload an Excel file with location data to visualize on historical maps.") | |
with gr.Row(): | |
with gr.Column(): | |
file_input = gr.File( | |
label="1. Upload Excel File", | |
file_types=[".xlsx", ".xls"], | |
type="filepath" | |
) | |
location_col = gr.Textbox( | |
label="2. Location Column Name", | |
value="location", | |
placeholder="e.g., 'city', 'address', or 'place'" | |
) | |
year = gr.Slider( | |
label="3. Historical Period (Year)", | |
minimum=1700, | |
maximum=2023, | |
value=1865, | |
step=1 | |
) | |
btn = gr.Button("Generate Map", variant="primary") | |
gr.Markdown(""" | |
### Tips: | |
- For best results, make sure location names are clear (e.g., "Paris, France" instead of just "Paris") | |
- If the map appears gray, try switching the tile layer using the layer control in the top-right | |
- You can measure distances and view the map in fullscreen using the controls | |
""") | |
with gr.Column(): | |
map_display = gr.HTML( | |
label="Historical Map", | |
value="<div style='text-align:center;padding:2em;color:gray;border:1px solid #ddd;height:70vh'>" | |
"Map will appear here after generation</div>" | |
) | |
stats = gr.Textbox(label="Map Information") | |
download = gr.File(label="Download Processed Data") | |
btn.click( | |
process_data, | |
inputs=[file_input, location_col, year], | |
outputs=[map_display, stats, download] | |
) | |
if __name__ == "__main__": | |
app.launch() |