oberbics commited on
Commit
dc6db03
·
verified ·
1 Parent(s): 5c72cc4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +182 -68
app.py CHANGED
@@ -1,130 +1,233 @@
1
  import gradio as gr
2
  import pandas as pd
3
  import folium
 
4
  from geopy.geocoders import Nominatim
5
  import tempfile
6
  import warnings
 
 
 
7
  from datetime import datetime
8
 
9
  warnings.filterwarnings("ignore")
10
 
11
- # Reliable Historical Tile Providers with fallbacks
12
  HISTORICAL_TILES = {
13
- "Colton 1865": {
14
- "url": "https://map1.davidrumsey.com/tiles/rumsey/SDSC1860/{z}/{x}/{y}.png",
15
- "attr": "David Rumsey Map Collection",
16
- "fallback": "https://stamen-tiles.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png",
17
- "years": (1800, 1900)
18
  },
19
- "Stamen 1915": {
20
- "url": "https://stamen-tiles.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png",
21
- "attr": "Stamen Maps",
22
  "fallback": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
23
  "years": (1901, 1920)
24
  },
25
- "OpenStreetMap": {
26
  "url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
27
  "attr": "OpenStreetMap",
28
  "fallback": None,
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  "years": (1700, 2023)
30
  }
31
  }
32
 
33
  class SafeGeocoder:
34
  def __init__(self):
35
- self.geolocator = Nominatim(user_agent="historical_mapper_v6", timeout=10)
 
 
 
 
 
 
 
 
 
 
 
36
 
37
  def get_coords(self, location: str):
 
 
 
 
 
 
 
 
 
 
38
  try:
 
39
  result = self.geolocator.geocode(location)
40
- return (result.latitude, result.longitude) if result else None
41
- except:
 
 
 
 
 
 
 
42
  return None
43
 
44
  def create_reliable_map(df, location_col, year):
45
- # Select appropriate tile configuration
46
- tile_config = next(
47
- (t for t in HISTORICAL_TILES.values()
48
- if t["years"][0] <= year <= t["years"][1]),
49
- HISTORICAL_TILES["OpenStreetMap"]
50
- )
51
 
52
- # Initialize map with fallback support
53
- m = folium.Map(location=[51.5, -0.1], zoom_start=5, control_scale=True)
 
 
 
 
54
 
55
- # Try primary tile layer
56
- primary_layer = folium.TileLayer(
57
- tiles=tile_config["url"],
58
- attr=tile_config["attr"],
59
- name=f"Primary ({year})",
60
- control=False
61
- ).add_to(m)
62
 
63
- # Add fallback layer
64
- if tile_config["fallback"]:
65
  folium.TileLayer(
66
- tiles=tile_config["fallback"],
67
- attr=f"{tile_config['attr']} (Fallback)",
68
- name="Fallback Tiles",
69
- control=False
 
 
70
  ).add_to(m)
71
 
72
- # Add OpenStreetMap as backup
73
- osm_layer = folium.TileLayer(
74
- tiles=HISTORICAL_TILES["OpenStreetMap"]["url"],
75
- attr=HISTORICAL_TILES["OpenStreetMap"]["attr"],
76
- name="OpenStreetMap",
77
- control=True
78
- ).add_to(m)
79
 
80
  # Add markers
81
  geocoder = SafeGeocoder()
82
  coords = []
83
- for loc in df[location_col].dropna().unique():
84
- point = geocoder.get_coords(str(loc))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  if point:
 
 
 
 
 
 
 
 
 
 
86
  folium.Marker(
87
  location=point,
88
- popup=f"<b>{loc}</b><br><i>Historical View ({year})</i>",
89
- icon=folium.Icon(color="blue")
90
- ).add_to(m)
 
 
91
  coords.append(point)
 
 
 
 
92
 
93
- # Layer control and bounds
94
- folium.LayerControl().add_to(m)
95
  if coords:
96
  m.fit_bounds(coords)
97
 
98
- # Force tile reload as workaround for gray tiles
99
  m.get_root().html.add_child(folium.Element("""
100
  <script>
101
- setTimeout(function() {
102
- const tiles = document.querySelectorAll('.leaflet-tile-loaded');
103
- tiles.forEach(tile => {
104
- const src = tile.src;
105
- tile.src = '';
106
- tile.src = src;
107
- });
108
- }, 1000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  </script>
110
  """))
111
 
112
- return m._repr_html_()
113
 
114
  def process_data(file_obj, location_col, year):
115
  try:
116
- df = pd.read_excel(file_obj.name)
 
 
 
 
117
 
 
118
  if location_col not in df.columns:
119
- return None, f"Column '{location_col}' not found", None
120
 
121
- map_html = create_reliable_map(df, location_col, year)
 
122
 
 
123
  with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp:
124
  df.to_excel(tmp.name, index=False)
125
  processed_path = tmp.name
126
 
127
- stats = f"Displaying {len(df)} locations from {year}"
 
 
 
 
 
128
  return (
129
  f"<div style='width:100%; height:70vh; border:1px solid #ddd'>{map_html}</div>",
130
  stats,
@@ -132,37 +235,48 @@ def process_data(file_obj, location_col, year):
132
  )
133
 
134
  except Exception as e:
 
 
 
135
  return None, f"Error: {str(e)}", None
136
 
137
  # Gradio Interface
138
- with gr.Blocks(title="Reliable Historical Maps", theme=gr.themes.Soft()) as app:
139
  gr.Markdown("# Historical Map Viewer")
 
140
 
141
  with gr.Row():
142
  with gr.Column():
143
  file_input = gr.File(
144
  label="1. Upload Excel File",
145
- file_types=[".xlsx"],
146
  type="filepath"
147
  )
148
  location_col = gr.Textbox(
149
  label="2. Location Column Name",
150
  value="location",
151
- placeholder="e.g., 'city' or 'address'"
152
  )
153
  year = gr.Slider(
154
- label="3. Map Year",
155
  minimum=1700,
156
- maximum=1920,
157
  value=1865,
158
  step=1
159
  )
160
  btn = gr.Button("Generate Map", variant="primary")
161
 
 
 
 
 
 
 
 
162
  with gr.Column():
163
  map_display = gr.HTML(
164
  label="Historical Map",
165
- value="<div style='text-align:center;padding:2em;color:gray;border:1px solid #ddd'>"
166
  "Map will appear here after generation</div>"
167
  )
168
  stats = gr.Textbox(label="Map Information")
 
1
  import gradio as gr
2
  import pandas as pd
3
  import folium
4
+ from folium.plugins import MeasureControl, Fullscreen, Search
5
  from geopy.geocoders import Nominatim
6
  import tempfile
7
  import warnings
8
+ import os
9
+ import time
10
+ import random
11
  from datetime import datetime
12
 
13
  warnings.filterwarnings("ignore")
14
 
15
+ # Updated Historical Tile Providers with reliable sources
16
  HISTORICAL_TILES = {
17
+ "Historical 1700s-1800s": {
18
+ "url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
19
+ "attr": "Esri",
20
+ "fallback": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}",
21
+ "years": (1700, 1900)
22
  },
23
+ "Early 1900s": {
24
+ "url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}",
25
+ "attr": "Esri",
26
  "fallback": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
27
  "years": (1901, 1920)
28
  },
29
+ "Modern Era": {
30
  "url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
31
  "attr": "OpenStreetMap",
32
  "fallback": None,
33
+ "years": (1921, 2023)
34
+ },
35
+ # Additional reliable tile sources
36
+ "Terrain": {
37
+ "url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Terrain_Base/MapServer/tile/{z}/{y}/{x}",
38
+ "attr": "Esri",
39
+ "fallback": None,
40
+ "years": (1700, 2023)
41
+ },
42
+ "Toner": {
43
+ "url": "https://tiles.stadiamaps.com/tiles/stamen_toner/{z}/{x}/{y}.png", # Updated Stamen source
44
+ "attr": "Stadia Maps",
45
+ "fallback": None,
46
  "years": (1700, 2023)
47
  }
48
  }
49
 
50
  class SafeGeocoder:
51
  def __init__(self):
52
+ user_agent = f"historical_mapper_v7_{random.randint(1000, 9999)}"
53
+ self.geolocator = Nominatim(user_agent=user_agent, timeout=10)
54
+ self.cache = {} # Simple cache to avoid repeated requests
55
+ self.last_request = 0
56
+
57
+ def _respect_rate_limit(self):
58
+ # Ensure at least 1 second between requests
59
+ current_time = time.time()
60
+ elapsed = current_time - self.last_request
61
+ if elapsed < 1.0:
62
+ time.sleep(1.0 - elapsed)
63
+ self.last_request = time.time()
64
 
65
  def get_coords(self, location: str):
66
+ if not location or pd.isna(location):
67
+ return None
68
+
69
+ # Convert to string if needed
70
+ location = str(location).strip()
71
+
72
+ # Check cache first
73
+ if location in self.cache:
74
+ return self.cache[location]
75
+
76
  try:
77
+ self._respect_rate_limit()
78
  result = self.geolocator.geocode(location)
79
+ if result:
80
+ coords = (result.latitude, result.longitude)
81
+ self.cache[location] = coords
82
+ return coords
83
+ self.cache[location] = None
84
+ return None
85
+ except Exception as e:
86
+ print(f"Geocoding error for '{location}': {e}")
87
+ self.cache[location] = None
88
  return None
89
 
90
  def create_reliable_map(df, location_col, year):
91
+ """Create a map with multiple layer options and better error handling"""
 
 
 
 
 
92
 
93
+ # Select appropriate default tile configuration based on year
94
+ default_tile_name = next(
95
+ (name for name, t in HISTORICAL_TILES.items()
96
+ if t["years"][0] <= year <= t["years"][1] and name in ["Historical 1700s-1800s", "Early 1900s", "Modern Era"]),
97
+ "Modern Era"
98
+ )
99
 
100
+ # Initialize map
101
+ m = folium.Map(location=[20, 0], zoom_start=2, control_scale=True)
 
 
 
 
 
102
 
103
+ # Add all tile layers with the appropriate one active
104
+ for name, config in HISTORICAL_TILES.items():
105
  folium.TileLayer(
106
+ tiles=config["url"],
107
+ attr=f"{config['attr']} ({name})",
108
+ name=name,
109
+ overlay=False,
110
+ control=True,
111
+ show=(name == default_tile_name) # Only show the default layer initially
112
  ).add_to(m)
113
 
114
+ # Add plugins for better user experience
115
+ Fullscreen().add_to(m)
116
+ MeasureControl(position='topright', primary_length_unit='kilometers').add_to(m)
 
 
 
 
117
 
118
  # Add markers
119
  geocoder = SafeGeocoder()
120
  coords = []
121
+
122
+ # Create marker cluster for better performance with many points
123
+ marker_cluster = folium.MarkerCluster(name="Locations").add_to(m)
124
+
125
+ # Process each location
126
+ processed_count = 0
127
+ for idx, row in df.iterrows():
128
+ if pd.isna(row[location_col]):
129
+ continue
130
+
131
+ location = str(row[location_col]).strip()
132
+
133
+ # Get additional info if available
134
+ additional_info = ""
135
+ for col in df.columns:
136
+ if col != location_col and not pd.isna(row[col]):
137
+ additional_info += f"<br><b>{col}:</b> {row[col]}"
138
+
139
+ # Geocode location
140
+ point = geocoder.get_coords(location)
141
  if point:
142
+ # Create popup content
143
+ popup_content = f"""
144
+ <div style="min-width: 200px; max-width: 300px">
145
+ <h4>{location}</h4>
146
+ <p><i>Historical View ({year})</i></p>
147
+ {additional_info}
148
+ </div>
149
+ """
150
+
151
+ # Add marker
152
  folium.Marker(
153
  location=point,
154
+ popup=folium.Popup(popup_content, max_width=300),
155
+ tooltip=location,
156
+ icon=folium.Icon(color="blue", icon="info-sign")
157
+ ).add_to(marker_cluster)
158
+
159
  coords.append(point)
160
+ processed_count += 1
161
+
162
+ # Layer control
163
+ folium.LayerControl(collapsed=False).add_to(m)
164
 
165
+ # Set bounds if we have coordinates
 
166
  if coords:
167
  m.fit_bounds(coords)
168
 
169
+ # Add better tile error handling with JavaScript
170
  m.get_root().html.add_child(folium.Element("""
171
  <script>
172
+ // Wait for the map to be fully loaded
173
+ document.addEventListener('DOMContentLoaded', function() {
174
+ setTimeout(function() {
175
+ // Get the map instance
176
+ var maps = document.querySelectorAll('.leaflet-container');
177
+ if (maps.length > 0) {
178
+ var map = maps[0];
179
+
180
+ // Add error handler for tiles
181
+ var layers = map.querySelectorAll('.leaflet-tile-pane .leaflet-layer');
182
+ for (var i = 0; i < layers.length; i++) {
183
+ var layer = layers[i];
184
+ var tiles = layer.querySelectorAll('.leaflet-tile');
185
+
186
+ // Check if layer has no loaded tiles
187
+ var loadedTiles = layer.querySelectorAll('.leaflet-tile-loaded');
188
+ if (tiles.length > 0 && loadedTiles.length === 0) {
189
+ // Force switch to OpenStreetMap if current layer failed
190
+ var osmButton = document.querySelector('.leaflet-control-layers-list input[type="radio"]:nth-child(3)');
191
+ if (osmButton) {
192
+ osmButton.click();
193
+ }
194
+ console.log("Switched to fallback tile layer due to loading issues");
195
+ }
196
+ }
197
+ }
198
+ }, 3000); // Wait 3 seconds for tiles to load
199
+ });
200
  </script>
201
  """))
202
 
203
+ return m._repr_html_(), processed_count
204
 
205
  def process_data(file_obj, location_col, year):
206
  try:
207
+ # Handle file reading
208
+ try:
209
+ df = pd.read_excel(file_obj.name)
210
+ except Exception as e:
211
+ return None, f"Error reading Excel file: {str(e)}", None
212
 
213
+ # Validate columns
214
  if location_col not in df.columns:
215
+ return None, f"Column '{location_col}' not found. Available columns: {', '.join(df.columns)}", None
216
 
217
+ # Create map
218
+ map_html, processed_count = create_reliable_map(df, location_col, year)
219
 
220
+ # Save processed data
221
  with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp:
222
  df.to_excel(tmp.name, index=False)
223
  processed_path = tmp.name
224
 
225
+ # Generate stats
226
+ total_locations = df[location_col].count()
227
+ success_rate = (processed_count / total_locations * 100) if total_locations > 0 else 0
228
+
229
+ stats = f"Found {processed_count} of {total_locations} locations ({success_rate:.1f}%) from year {year}"
230
+
231
  return (
232
  f"<div style='width:100%; height:70vh; border:1px solid #ddd'>{map_html}</div>",
233
  stats,
 
235
  )
236
 
237
  except Exception as e:
238
+ import traceback
239
+ error_details = traceback.format_exc()
240
+ print(f"Error in processing: {error_details}")
241
  return None, f"Error: {str(e)}", None
242
 
243
  # Gradio Interface
244
+ with gr.Blocks(title="Historical Maps", theme=gr.themes.Soft()) as app:
245
  gr.Markdown("# Historical Map Viewer")
246
+ gr.Markdown("Upload an Excel file with location data to visualize on historical maps.")
247
 
248
  with gr.Row():
249
  with gr.Column():
250
  file_input = gr.File(
251
  label="1. Upload Excel File",
252
+ file_types=[".xlsx", ".xls"],
253
  type="filepath"
254
  )
255
  location_col = gr.Textbox(
256
  label="2. Location Column Name",
257
  value="location",
258
+ placeholder="e.g., 'city', 'address', or 'place'"
259
  )
260
  year = gr.Slider(
261
+ label="3. Historical Period (Year)",
262
  minimum=1700,
263
+ maximum=2023,
264
  value=1865,
265
  step=1
266
  )
267
  btn = gr.Button("Generate Map", variant="primary")
268
 
269
+ gr.Markdown("""
270
+ ### Tips:
271
+ - For best results, make sure location names are clear (e.g., "Paris, France" instead of just "Paris")
272
+ - If the map appears gray, try switching the tile layer using the layer control in the top-right
273
+ - You can measure distances and view the map in fullscreen using the controls
274
+ """)
275
+
276
  with gr.Column():
277
  map_display = gr.HTML(
278
  label="Historical Map",
279
+ value="<div style='text-align:center;padding:2em;color:gray;border:1px solid #ddd;height:70vh'>"
280
  "Map will appear here after generation</div>"
281
  )
282
  stats = gr.Textbox(label="Map Information")