mdAmin313 commited on
Commit
1bdf519
·
verified ·
1 Parent(s): c00c394

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +207 -0
app.py CHANGED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ import gradio as gr
3
+ import requests
4
+ import pycountry
5
+ import pandas as pd
6
+ import folium
7
+ from folium.plugins import MarkerCluster
8
+ import io
9
+ import base64
10
+ import time
11
+
12
+ # --- Helpers --------------------------------------------------------------
13
+
14
+ USER_AGENT = "hf-space-poi-finder/1.0 (your_email@example.com)"
15
+
16
+ def list_countries():
17
+ items = sorted([(c.name, c.alpha_2) for c in pycountry.countries], key=lambda x: x[0])
18
+ names = [name for name, code in items]
19
+ codes = [code for name, code in items]
20
+ # return mapping lists so gradio can show names but we keep codes
21
+ return names, codes
22
+
23
+ # Return list of subdivision names and a matching list of their codes (ISO 3166-2)
24
+ def list_subdivisions(country_code):
25
+ subs = list(pycountry.subdivisions.get(country_code=country_code))
26
+ if not subs:
27
+ return [], []
28
+ items = sorted([(s.name, s.code) for s in subs], key=lambda x: x[0])
29
+ names = [n for n, c in items]
30
+ codes = [c for n, c in items]
31
+ return names, codes
32
+
33
+ # Use Nominatim to geocode the subdivision name (state) inside country to get bbox
34
+ def geocode_region(name, country_code):
35
+ url = "https://nominatim.openstreetmap.org/search"
36
+ params = {
37
+ "q": f"{name}, {country_code}",
38
+ "format": "json",
39
+ "limit": 1,
40
+ "polygon_geojson": 0,
41
+ "addressdetails": 0,
42
+ }
43
+ headers = {"User-Agent": USER_AGENT}
44
+ resp = requests.get(url, params=params, headers=headers, timeout=30)
45
+ resp.raise_for_status()
46
+ data = resp.json()
47
+ if not data:
48
+ return None
49
+ item = data[0]
50
+ # Nominatim returns boundingbox as [south, north, west, east] (strings)
51
+ bbox = [float(item["boundingbox"][0]), float(item["boundingbox"][1]),
52
+ float(item["boundingbox"][2]), float(item["boundingbox"][3])]
53
+ # convert to Overpass bbox ordering: south,west,north,east
54
+ return (bbox[0], bbox[2], bbox[1], bbox[3])
55
+
56
+ # Build Overpass API query to fetch nodes/ways/relations with amenity tags in bbox
57
+ def overpass_query(amenities, bbox, timeout=60):
58
+ # amenities: list like ["cafe","motel","hotel"]
59
+ south, west, north, east = bbox
60
+ amen_regex = "|".join([a for a in amenities])
61
+ # query nodes, ways, relations; output center for ways/relations
62
+ q = f"""
63
+ [out:json][timeout:{timeout}];
64
+ (
65
+ node["amenity"~"^{amen_regex}$"]({south},{west},{north},{east});
66
+ way["amenity"~"^{amen_regex}$"]({south},{west},{north},{east});
67
+ relation["amenity"~"^{amen_regex}$"]({south},{west},{north},{east});
68
+ );
69
+ out center tags;
70
+ """
71
+ return q
72
+
73
+ def fetch_places(amenities, bbox):
74
+ q = overpass_query(amenities, bbox)
75
+ url = "https://overpass-api.de/api/interpreter"
76
+ headers = {"User-Agent": USER_AGENT}
77
+ resp = requests.post(url, data=q.encode("utf-8"), headers=headers, timeout=120)
78
+ resp.raise_for_status()
79
+ data = resp.json()
80
+ elements = data.get("elements", [])
81
+ rows = []
82
+ for el in elements:
83
+ tags = el.get("tags", {})
84
+ name = tags.get("name") or tags.get("name:en") or ""
85
+ amenity = tags.get("amenity", "")
86
+ # coords: node has lat/lon; way/relation use 'center'
87
+ if el.get("type") == "node":
88
+ lat = el.get("lat")
89
+ lon = el.get("lon")
90
+ else:
91
+ center = el.get("center")
92
+ lat = center.get("lat") if center else None
93
+ lon = center.get("lon") if center else None
94
+ address = ", ".join(v for k, v in tags.items() if k.startswith("addr:")) or ""
95
+ rows.append({
96
+ "name": name,
97
+ "amenity": amenity,
98
+ "lat": lat,
99
+ "lon": lon,
100
+ "address": address,
101
+ "osm_id": f'{el.get("type")}/{el.get("id")}',
102
+ "tags": tags
103
+ })
104
+ df = pd.DataFrame(rows)
105
+ # remove ones without coordinates
106
+ df = df.dropna(subset=["lat", "lon"])
107
+ # reorder columns
108
+ if not df.empty:
109
+ df = df[["name","amenity","lat","lon","address","osm_id","tags"]]
110
+ return df
111
+
112
+ def df_to_csv_bytes(df):
113
+ buf = io.StringIO()
114
+ df.to_csv(buf, index=False)
115
+ return buf.getvalue().encode("utf-8")
116
+
117
+ def make_map(df, center_bbox=None):
118
+ # center map on bbox if provided else on mean coordinates
119
+ if df.empty:
120
+ # empty map default world view
121
+ m = folium.Map(location=[20,0], zoom_start=2)
122
+ return m._repr_html_()
123
+ if center_bbox:
124
+ south, west, north, east = center_bbox
125
+ center_lat = (south + north) / 2.0
126
+ center_lon = (west + east) / 2.0
127
+ zoom = 8
128
+ else:
129
+ center_lat = df["lat"].mean()
130
+ center_lon = df["lon"].mean()
131
+ zoom = 10
132
+ m = folium.Map(location=[center_lat, center_lon], zoom_start=zoom)
133
+ cluster = MarkerCluster().add_to(m)
134
+ for _, row in df.iterrows():
135
+ name = row.get("name") or row.get("amenity")
136
+ popup_html = f"<b>{name}</b><br>{row.get('amenity')}<br>{row.get('address')}"
137
+ folium.Marker([row["lat"], row["lon"]], popup=popup_html).add_to(cluster)
138
+ return m._repr_html_()
139
+
140
+ # --- Gradio interface functions -------------------------------------------
141
+
142
+ COUNTRY_NAMES, COUNTRY_CODES = list_countries()
143
+
144
+ def update_states(selected_country_name):
145
+ # find index
146
+ try:
147
+ idx = COUNTRY_NAMES.index(selected_country_name)
148
+ code = COUNTRY_CODES[idx]
149
+ except ValueError:
150
+ return gr.update(choices=[]), gr.update(visible=False)
151
+ names, codes = list_subdivisions(code)
152
+ if not names:
153
+ # no subdivisions in pycountry for that country
154
+ return gr.update(choices=[]), gr.update(visible=False)
155
+ return gr.update(choices=names, value=names[0]), gr.update(visible=True)
156
+
157
+ def run_search(country_name, state_name, categories):
158
+ start = time.time()
159
+ # get country code
160
+ try:
161
+ idx = COUNTRY_NAMES.index(country_name)
162
+ country_code = COUNTRY_CODES[idx]
163
+ except ValueError:
164
+ return "Invalid country", None, None, None
165
+ # geocode state
166
+ bbox = geocode_region(state_name if state_name else country_name, country_code)
167
+ if bbox is None:
168
+ return f"Could not geocode region '{state_name}'. Try a different subdivision or broaden to the country.", None, None, None
169
+ if not categories:
170
+ return "Please select at least one category.", None, None, None
171
+ df = fetch_places(categories, bbox)
172
+ map_html = make_map(df, center_bbox=bbox)
173
+ csv_bytes = df_to_csv_bytes(df)
174
+ elapsed = time.time() - start
175
+ msg = f"Found {len(df)} places for {', '.join(categories)} in {state_name}, {country_name} (took {elapsed:.1f}s)."
176
+ # prepare downloadable link (data URI)
177
+ csv_b64 = base64.b64encode(csv_bytes).decode("utf-8")
178
+ csv_href = f"data:text/csv;base64,{csv_b64}"
179
+ return msg, df, map_html, csv_href
180
+
181
+ # --- Build Gradio UI -----------------------------------------------------
182
+
183
+ place_options = ["cafe","motel","hotel","restaurant","bar","pub","bakery","fast_food","guest_house","hostel"]
184
+
185
+ with gr.Blocks() as demo:
186
+ gr.Markdown("# Client-finder SaaS dashboard (OSM) — deploy on Hugging Face Spaces")
187
+ with gr.Row():
188
+ with gr.Column(scale=1):
189
+ country = gr.Dropdown(choices=COUNTRY_NAMES, value="United States", label="Country")
190
+ state = gr.Dropdown(choices=[], label="State / Subdivision")
191
+ update_btn = gr.Button("Refresh states")
192
+ categories = gr.CheckboxGroup(place_options, label="Categories to search", value=["cafe","restaurant"])
193
+ search_btn = gr.Button("Search places")
194
+ info = gr.Textbox(label="Status", interactive=False)
195
+ download = gr.File(label="Download results (CSV)", interactive=False)
196
+ with gr.Column(scale=2):
197
+ map_html_out = gr.HTML(label="Map")
198
+ table_out = gr.Dataframe(headers=["name","amenity","lat","lon","address","osm_id"], label="Results Table")
199
+
200
+ # initial population of states for default country
201
+ state.update(choices=list_subdivisions("US")[0], value=(list_subdivisions("US")[0][0] if list_subdivisions("US")[0] else None))
202
+
203
+ update_btn.click(fn=update_states, inputs=country, outputs=[state, state], _js=None)
204
+ search_btn.click(fn=run_search, inputs=[country, state, categories], outputs=[info, table_out, map_html_out, download])
205
+
206
+ if __name__ == "__main__":
207
+ demo.launch()