Geoeasy commited on
Commit
e02596d
·
verified ·
1 Parent(s): 74bf6f8

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +398 -0
  2. requiremets.txt +5 -0
app.py ADDED
@@ -0,0 +1,398 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py - VERSÃO FINAL CORRIGIDA
2
+ import os
3
+ import re
4
+ import zipfile
5
+ import tempfile
6
+ import gradio as gr
7
+ from geopy.geocoders import Nominatim
8
+ import osmnx as ox
9
+ import folium
10
+ import branca
11
+ import pandas as pd
12
+ import geopandas as gpd
13
+
14
+ # --- Initial settings ---
15
+ geolocator = Nominatim(user_agent="gradio_osm_app")
16
+ ox.settings.log_console = False
17
+
18
+ # CORRIGIDO: Usar diretório de trabalho atual ou temp do sistema
19
+ # Opção 1: Usar diretório "downloads" no diretório de trabalho atual
20
+ DOWNLOAD_DIR = os.path.join(os.getcwd(), "downloads")
21
+ os.makedirs(DOWNLOAD_DIR, exist_ok=True)
22
+
23
+ # Se o diretório de trabalho não for acessível, usar temp do sistema
24
+ if not os.access(DOWNLOAD_DIR, os.W_OK):
25
+ DOWNLOAD_DIR = tempfile.gettempdir()
26
+ print(f"⚠️ Usando diretório temporário do sistema: {DOWNLOAD_DIR}")
27
+ else:
28
+ print(f"✅ Usando diretório de downloads: {DOWNLOAD_DIR}")
29
+
30
+ def slugify(name: str) -> str:
31
+ return re.sub(r"[^0-9A-Za-z]+", "_", name).strip("_")
32
+
33
+ # --------- Utils for safe GPKG writing ----------
34
+ def _make_unique(names):
35
+ """Make column names unique by suffixing __1, __2 on duplicates."""
36
+ seen = {}
37
+ out = []
38
+ for n in names:
39
+ n0 = str(n)
40
+ cnt = seen.get(n0, 0)
41
+ out.append(n0 if cnt == 0 else f"{n0}__{cnt}")
42
+ seen[n0] = cnt + 1
43
+ return out
44
+
45
+ def _is_complex_obj(v):
46
+ # Types OGR doesn't accept as field values
47
+ return isinstance(v, (list, dict, tuple, set, bytes))
48
+
49
+ def clean_for_gpkg(gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
50
+ """Sanitize columns and dtypes for safe GPKG write."""
51
+ gdf = gdf.copy()
52
+
53
+ # Ensure CRS
54
+ if getattr(gdf, "crs", None) is None:
55
+ try:
56
+ gdf.set_crs(4326, inplace=True)
57
+ except Exception:
58
+ pass
59
+
60
+ # Normalize column names
61
+ cols = pd.Index(gdf.columns).map(str)
62
+ cols = cols.str.replace(r"[^0-9a-zA-Z_]", "_", regex=True)\
63
+ .str.replace(r"_{2,}", "_", regex=True)\
64
+ .str.strip("_")\
65
+ .str.slice(0, 60)
66
+
67
+ # Guarantee uniqueness (avoids gdf.dtypes[col] returning a Series)
68
+ if cols.duplicated().any() or isinstance(cols, pd.MultiIndex):
69
+ cols = pd.Index(_make_unique(cols))
70
+ gdf.columns = cols
71
+
72
+ geom_name = gdf.geometry.name if hasattr(gdf, "geometry") and gdf.geometry is not None else None
73
+
74
+ # Convert problematic dtypes
75
+ for col in gdf.columns:
76
+ if col == geom_name:
77
+ continue
78
+
79
+ dt = gdf.dtypes[col]
80
+ # Defensive: if dt ever comes as Series, collapse to string
81
+ if isinstance(dt, pd.Series):
82
+ gdf[col] = gdf[col].astype("string").fillna("")
83
+ continue
84
+
85
+ # tz-aware datetimes -> naive
86
+ if pd.api.types.is_datetime64tz_dtype(dt):
87
+ gdf[col] = gdf[col].dt.tz_convert(None)
88
+
89
+ # objects -> string (if complex) or pandas string dtype
90
+ if pd.api.types.is_object_dtype(dt):
91
+ if gdf[col].apply(_is_complex_obj).any():
92
+ gdf[col] = gdf[col].astype(str)
93
+ else:
94
+ gdf[col] = gdf[col].astype("string")
95
+
96
+ # categorical -> string
97
+ if pd.api.types.is_categorical_dtype(dt):
98
+ gdf[col] = gdf[col].astype(str)
99
+
100
+ # nullable integer -> float64
101
+ if pd.api.types.is_integer_dtype(dt) and str(dt).startswith("Int"):
102
+ gdf[col] = gdf[col].astype("float64")
103
+
104
+ # booleans with NaN -> string; pure bool stays
105
+ if pd.api.types.is_bool_dtype(dt):
106
+ if gdf[col].isna().any():
107
+ gdf[col] = gdf[col].astype("string").fillna("")
108
+
109
+ # fill NaN in pandas string dtype
110
+ if gdf[col].dtype == "string":
111
+ gdf[col] = gdf[col].fillna("")
112
+
113
+ # Final friendly names
114
+ gdf.rename(columns=lambda c: str(c).strip("_")[:60], inplace=True)
115
+
116
+ return gdf
117
+
118
+ def try_to_file(gdf: gpd.GeoDataFrame, path: str, driver: str = "GPKG"):
119
+ """Try to save; on field error, drop only the offending field and retry once."""
120
+ try:
121
+ gdf.to_file(path, driver=driver)
122
+ return
123
+ except Exception as e:
124
+ msg = str(e)
125
+ m = re.search(r"field '([^']+)'", msg, flags=re.IGNORECASE)
126
+ if m:
127
+ bad = m.group(1)
128
+ if bad in gdf.columns:
129
+ gdf2 = gdf.drop(columns=[bad])
130
+ gdf2.to_file(path, driver=driver)
131
+ return
132
+ raise
133
+
134
+ def ensure_saved(gdf: gpd.GeoDataFrame, slug: str, layer: str):
135
+ if gdf is None or gdf.empty:
136
+ return
137
+ # Extra guard: unique columns before cleaning/writing
138
+ if pd.Index(gdf.columns).duplicated().any():
139
+ gdf = gdf.copy()
140
+ gdf.columns = pd.Index(_make_unique(pd.Index(gdf.columns).map(str)))
141
+
142
+ filename = f"{slug}_{layer}.gpkg"
143
+ path = os.path.join(DOWNLOAD_DIR, filename)
144
+ if os.path.exists(path):
145
+ os.remove(path)
146
+
147
+ gdf_clean = clean_for_gpkg(gdf)
148
+
149
+ try:
150
+ try_to_file(gdf_clean, path, driver="GPKG")
151
+ print(f"✅ Arquivo salvo com sucesso: {path}")
152
+ except Exception:
153
+ # Drop common problematic OSM fields as a second attempt
154
+ drop_candidates = [c for c in gdf_clean.columns if c.lower() in {"fixme", "note", "source_ref"}]
155
+ if drop_candidates:
156
+ gdf_clean2 = gdf_clean.drop(columns=drop_candidates, errors="ignore")
157
+ try_to_file(gdf_clean2, path, driver="GPKG")
158
+ else:
159
+ # Last resort: stringify all non-geometry columns
160
+ gdf_last = gdf_clean.copy()
161
+ for c in gdf_last.columns:
162
+ if c != gdf_last.geometry.name:
163
+ gdf_last[c] = gdf_last[c].astype(str)
164
+ try_to_file(gdf_last, path, driver="GPKG")
165
+ print(f"✅ Arquivo salvo com sucesso (com tratamento): {path}")
166
+
167
+ # --------- UI / main logic ----------
168
+ def make_legend(selected_layers):
169
+ color_map = {
170
+ "Highways": "yellow",
171
+ "Buildings": "#ff802a",
172
+ "School": "blue",
173
+ "Fire Station": "red",
174
+ "Hospital": "green",
175
+ "Police Station": "lightblue",
176
+ "Restaurants": "orange",
177
+ "Hotels": "purple",
178
+ "Monuments": "lightgreen"
179
+ }
180
+ html = '''
181
+ <div style="
182
+ position: fixed; bottom: 50px; left: 10px;
183
+ background-color: rgba(0,0,0,0.7); color: white;
184
+ padding: 10px; font-size:14px; border-radius:5px; z-index:9999;
185
+ ">
186
+ <b>Legend</b><br>
187
+ '''
188
+ for layer in selected_layers:
189
+ color = color_map.get(layer, "gray")
190
+ html += f'''
191
+ <i style="background: {color}; width:10px;height:10px;display:inline-block;margin-right:5px;"></i>{layer}<br>
192
+ '''
193
+ html += '</div>'
194
+ return branca.element.Element(html)
195
+
196
+ style_funcs = {
197
+ "Highways": lambda f: {"color": "yellow", "weight": 0.5},
198
+ "Buildings": lambda f: {"fill": True, "fillColor": "#ff802a", "color": "#ff802a", "weight": 0.5, "fillOpacity": 0.6},
199
+ "School": lambda f: {"fill": True, "fillColor": "blue", "color": "blue", "weight": 0.5, "fillOpacity": 0.6},
200
+ "Fire Station": lambda f: {"fill": True, "fillColor": "red", "color": "red", "weight": 0.5, "fillOpacity": 0.6},
201
+ "Hospital": lambda f: {"fill": True, "fillColor": "green", "color": "green", "weight": 0.5, "fillOpacity": 0.6},
202
+ "Police Station": lambda f: {"fill": True, "fillColor": "lightblue", "color": "lightblue", "weight": 0.5, "fillOpacity": 0.6},
203
+ "Restaurants": lambda f: {"fill": True, "fillColor": "orange", "color": "orange", "weight": 0.5, "fillOpacity": 0.6},
204
+ "Hotels": lambda f: {"fill": True, "fillColor": "purple", "color": "purple", "weight": 0.5, "fillOpacity": 0.6},
205
+ "Monuments": lambda f: {"fill": True, "fillColor": "lightgreen", "color": "lightgreen", "weight": 0.5, "fillOpacity": 0.6},
206
+ }
207
+
208
+ def map_with_layers(place_name, cb_highways, cb_buildings, cb_school,
209
+ cb_fire, cb_hospital, cb_police, cb_rest, cb_hotels, cb_monuments):
210
+ if not place_name or not place_name.strip():
211
+ yield None, "❌ Please enter a valid place name."
212
+ return
213
+ slug = slugify(place_name)
214
+
215
+ yield None, "🔄 Geocoding..."
216
+ try:
217
+ gdf_place = ox.geocode_to_gdf(place_name)
218
+ poly = gdf_place.geometry.iloc[0]
219
+ lat, lon = poly.centroid.y, poly.centroid.x
220
+ except Exception as e:
221
+ yield None, f"❌ Geocoding error: {e}"
222
+ return
223
+
224
+ layers = {}
225
+
226
+ # Salvar highways com o nome correto
227
+ if cb_highways:
228
+ yield None, "🔄 Downloading Highways..."
229
+ try:
230
+ G = ox.graph_from_polygon(poly, network_type="all", simplify=True)
231
+ gdf = ox.graph_to_gdfs(G, nodes=False, edges=True)
232
+ if not gdf.empty:
233
+ layers['Highways'] = gdf
234
+ ensure_saved(gdf, slug, 'highways')
235
+ yield None, f"✅ Highways downloaded: {len(gdf)} features"
236
+ except Exception as e:
237
+ yield None, f"⚠️ Highways error: {e}"
238
+
239
+ tag_map = {
240
+ 'Buildings': {'building': True},
241
+ 'School': {'amenity': 'school'},
242
+ 'Fire Station': {'amenity': 'fire_station'},
243
+ 'Hospital': {'amenity': 'hospital'},
244
+ 'Police Station': {'amenity': 'police'},
245
+ 'Restaurants': {'amenity': 'restaurant'},
246
+ 'Hotels': {'tourism': 'hotel'},
247
+ 'Monuments': {'historic': 'monument'}
248
+ }
249
+ flags = [cb_buildings, cb_school, cb_fire, cb_hospital,
250
+ cb_police, cb_rest, cb_hotels, cb_monuments]
251
+ for (name, tags), flag in zip(tag_map.items(), flags):
252
+ if flag:
253
+ yield None, f"🔄 Downloading {name}..."
254
+ try:
255
+ gdf2 = ox.features_from_polygon(poly, tags)
256
+ # Only polygons for these layers
257
+ gdf2 = gdf2[gdf2.geometry.type.isin(['Polygon', 'MultiPolygon'])]
258
+ if not gdf2.empty:
259
+ layers[name] = gdf2
260
+ layer_name = name.replace(' ', '').lower()
261
+ ensure_saved(gdf2, slug, layer_name)
262
+ yield None, f"✅ {name} downloaded: {len(gdf2)} features"
263
+ except Exception as e:
264
+ yield None, f"⚠️ {name} error: {e}"
265
+
266
+ yield None, "🔄 Rendering map..."
267
+ m = folium.Map([lat, lon], zoom_start=13, tiles='CartoDB Dark_Matter')
268
+ for name, gdf in layers.items():
269
+ if gdf.empty:
270
+ continue
271
+ has_name = 'name' in gdf.columns and gdf['name'].notna().any()
272
+ cols = ['geometry', 'name'] if has_name else ['geometry']
273
+ gj = folium.GeoJson(gdf[cols], name=name, style_function=style_funcs.get(name, lambda f: {}))
274
+ if has_name:
275
+ gj.add_child(folium.GeoJsonPopup(fields=['name'], labels=False))
276
+ gj.add_to(m)
277
+
278
+ folium.LayerControl(collapsed=False).add_to(m)
279
+ m.get_root().html.add_child(make_legend(list(layers.keys())))
280
+
281
+ html_path = os.path.join(DOWNLOAD_DIR, f"{slug}_map.html")
282
+ m.save(html_path)
283
+ yield m._repr_html_(), '✅ Map ready!'
284
+
285
+ def download_data(place_name, cb_highways, cb_buildings, cb_school,
286
+ cb_fire, cb_hospital, cb_police, cb_rest, cb_hotels, cb_monuments):
287
+ slug = slugify(place_name)
288
+ zip_path = os.path.join(DOWNLOAD_DIR, f"{slug}_osm_data.zip")
289
+ print("Download iniciado para:", slug)
290
+
291
+ try:
292
+ if os.path.exists(zip_path):
293
+ print("ZIP já existe:", zip_path)
294
+ return zip_path
295
+
296
+ files = []
297
+
298
+ # Mapeamento consistente de nomes de camadas
299
+ layer_mapping = {
300
+ 'highways': cb_highways,
301
+ 'buildings': cb_buildings,
302
+ 'school': cb_school,
303
+ 'fire_station': cb_fire,
304
+ 'hospital': cb_hospital,
305
+ 'police_station': cb_police,
306
+ 'restaurants': cb_rest,
307
+ 'hotels': cb_hotels,
308
+ 'monuments': cb_monuments
309
+ }
310
+
311
+ for layer, flag in layer_mapping.items():
312
+ if flag:
313
+ path = os.path.join(DOWNLOAD_DIR, f"{slug}_{layer}.gpkg")
314
+ if os.path.exists(path):
315
+ print(f"✅ Arquivo encontrado: {path}")
316
+ files.append(path)
317
+ else:
318
+ print(f"⚠️ Arquivo não encontrado: {path}")
319
+
320
+ html_path = os.path.join(DOWNLOAD_DIR, f"{slug}_map.html")
321
+ if os.path.exists(html_path):
322
+ files.append(html_path)
323
+
324
+ if not files:
325
+ print("Nenhum arquivo para compactar.")
326
+ raise gr.Error('❌ No layer to download.')
327
+
328
+ print("Arquivos para zipar:", files)
329
+
330
+ with zipfile.ZipFile(zip_path, 'w', compression=zipfile.ZIP_STORED) as zipf:
331
+ for f in files:
332
+ zipf.write(f, arcname=os.path.basename(f))
333
+
334
+ print("ZIP gerado com sucesso:", zip_path)
335
+ return zip_path
336
+ except Exception as e:
337
+ print("Erro ao gerar o ZIP:", str(e))
338
+ raise gr.Error(f"❌ Download failed: {e}")
339
+
340
+ # --- Layout with tabs ---
341
+ with gr.Blocks(title="Geoeasy View") as demo:
342
+ gr.HTML("""
343
+ <style>
344
+ #logo { float: left; }
345
+ #app-title { display: inline-block; vertical-align: middle; margin: 0 0 0 1em; }
346
+ .header-wrapper { overflow: auto; margin-bottom: 1em; }
347
+ </style>
348
+ """)
349
+ with gr.Tabs():
350
+ with gr.TabItem('Map'):
351
+ gr.HTML("""
352
+ <div class="header-wrapper">
353
+ <h1 id="app-title">OSM View</h1>
354
+ </div>
355
+ """)
356
+ inp = gr.Textbox(label='Place', placeholder='e.g.: Ellwangen, Germany')
357
+ with gr.Row():
358
+ cb_highways = gr.Checkbox(label='Highways', value=True)
359
+ cb_buildings = gr.Checkbox(label='Buildings', value=True)
360
+ cb_school = gr.Checkbox(label='School')
361
+ cb_fire = gr.Checkbox(label='Fire Station')
362
+ cb_hospital = gr.Checkbox(label='Hospital')
363
+ with gr.Row():
364
+ cb_police = gr.Checkbox(label='Police Station')
365
+ cb_rest = gr.Checkbox(label='Restaurants')
366
+ cb_hotels = gr.Checkbox(label='Hotels')
367
+ cb_monuments = gr.Checkbox(label='Monuments')
368
+ btn_map = gr.Button('Show Map')
369
+ btn_dl = gr.Button('Download Files')
370
+ out_map, out_status = gr.HTML(), gr.Textbox(interactive=False)
371
+ out_files = gr.File(label="Download ZIP", type="filepath")
372
+
373
+ inputs = [inp, cb_highways, cb_buildings, cb_school, cb_fire,
374
+ cb_hospital, cb_police, cb_rest, cb_hotels, cb_monuments]
375
+ btn_map.click(map_with_layers, inputs=inputs, outputs=[out_map, out_status])
376
+ btn_dl.click(download_data, inputs=inputs, outputs=out_files, show_progress=True)
377
+
378
+ with gr.TabItem('Taginfo'):
379
+ gr.Markdown('''
380
+ **Most used keys (Taginfo)**
381
+ | Position | Key | Approximate usage¹ |
382
+ |----------|------------|----------------------------|
383
+ | 1 | `highway` | ~58 million geometries |
384
+ | 2 | `name` | ~55 million geometries |
385
+ | 3 | `source` | ~52 million geometries |
386
+ | 4 | `building` | ~48 million geometries |
387
+ | 5 | `landuse` | ~34 million geometries |
388
+ | 6 | `natural` | ~20 million geometries |
389
+ | 7 | `waterway` | ~18 million geometries |
390
+ | 8 | `amenity` | ~16 million geometries |
391
+ | 9 | `place` | ~12 million geometries |
392
+ | 10 | `power` | ~14 million geometries |
393
+ ¹ Approximate values from daily Taginfo reports.
394
+ ''')
395
+
396
+ if __name__ == '__main__':
397
+ # CORRIGIDO: Adicionar allowed_paths para permitir downloads do diretório de downloads
398
+ demo.launch(allowed_paths=[DOWNLOAD_DIR])
requiremets.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio
2
+ osmnx
3
+ geopy
4
+ folium
5
+ branca