celikn commited on
Commit
2e5e8b0
·
verified ·
1 Parent(s): 68c1fb8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +794 -308
app.py CHANGED
@@ -1,10 +1,8 @@
1
  import os
2
- import json
3
  import gradio as gr
4
  import geopandas as gpd
5
  import folium
6
  import requests
7
- from shapely.geometry import shape
8
 
9
  # osmnx opsiyonel (fallback için), yoksa sorun değil
10
  try:
@@ -14,6 +12,7 @@ except ImportError:
14
 
15
  from huggingface_hub import InferenceClient, login
16
 
 
17
  # ==========================================
18
  # HF TOKEN & MODEL
19
  # ==========================================
@@ -24,6 +23,14 @@ if HF_TOKEN:
24
  else:
25
  print("UYARI: HF_TOKEN / HUGGINGFACE_HUB_TOKEN bulunamadı, gated modellere erişilemeyebilir.")
26
 
 
 
 
 
 
 
 
 
27
  # ==========================================
28
  # ÖNCEDEN HAZIRLANMIŞ OSM VERİSİ
29
  # ==========================================
@@ -41,36 +48,25 @@ else:
41
 
42
 
43
  DEFAULT_TAGS = {
44
- "amenity": ["school", "pharmacy", "hospital", "restaurant", "cafe", "bank", "university"],
45
  "leisure": ["park", "playground"],
46
  "shop": True,
47
- "highway": ["bus_stop"],
48
- "railway": ["station", "halt", "tram_stop"],
49
- "public_transport": ["stop_position", "platform"],
 
50
  }
51
 
52
- # ==========================================
53
- # SYSTEM PROMPTS (GÜNCELLENDİ)
54
- # ==========================================
55
- # <<< GÜNCELLENDİ: LLM'e 'around' (yakınında) sorgusu yapmayı öğretiyoruz.
56
- OSM_QUERY_SYSTEM = """
57
- You are an expert in OpenStreetMap and Overpass API.
58
- Produce only valid Overpass QL.
59
- No explanations. No markdown.
60
- Use [out:json][timeout:25]; at top.
61
- End with: out center;
62
 
63
- IMPORTANT RULES:
64
- 1. If the user asks for "nearest", "nearby", "close to" or "closest", use the (around:RADIUS, LAT, LON) filter.
65
- Example: node(around:3000, 39.123, 32.456)["amenity"="university"];
66
- 2. Do not use 'center' area unless explicitly asked. Use the provided lat/lon coordinates in the prompt context.
67
- 3. Default radius for "nearby" queries should be around 2000 to 5000 meters if not specified.
68
- """
69
 
70
  # ==========================================
71
  # OSM / CBS FONKSİYONLARI
72
  # ==========================================
73
  def get_neighborhood_gdf(city: str, district: str, neighborhood: str):
 
 
 
 
74
  city = (city or "").strip()
75
  district = (district or "").strip()
76
  neighborhood = (neighborhood or "").strip()
@@ -88,9 +84,13 @@ def get_neighborhood_gdf(city: str, district: str, neighborhood: str):
88
 
89
  # 2) Fallback: canlı OSM çağrısı
90
  if ox is None:
 
91
  return None
92
 
 
93
  query = f"{neighborhood}, {district}, {city}, Türkiye"
 
 
94
  try:
95
  gdf_osm = ox.geocode_to_gdf(query)
96
  except Exception as e:
@@ -104,17 +104,27 @@ def get_neighborhood_gdf(city: str, district: str, neighborhood: str):
104
  gdf_osm["city"] = city
105
  gdf_osm["district"] = district
106
  gdf_osm["neighborhood"] = neighborhood
 
107
  return gdf_osm
108
 
109
 
 
110
  def get_pois_within(gdf, tags=None):
 
 
 
 
111
  if gdf is None:
112
  return None
 
113
  if tags is None:
114
  tags = DEFAULT_TAGS
115
 
116
  # 1) Precomputed POI verisi
117
- if (pois_gdf is not None and all(col in gdf.columns for col in ["city", "district", "neighborhood"])):
 
 
 
118
  row = gdf.iloc[0]
119
  city = row["city"]
120
  district = row["district"]
@@ -126,11 +136,13 @@ def get_pois_within(gdf, tags=None):
126
  & (pois_gdf["neighborhood"] == neighborhood)
127
  )
128
  pois_local = pois_gdf[mask]
 
129
  if pois_local is not None and len(pois_local) > 0:
130
  return pois_local
131
 
132
  # 2) Fallback: OSM'den canlı POI çek
133
  if ox is None:
 
134
  return None
135
 
136
  try:
@@ -144,11 +156,13 @@ def get_pois_within(gdf, tags=None):
144
 
145
  def summarize_pois(gdf, pois):
146
  summary = {}
 
147
  try:
148
  area_m2 = gdf.to_crs(epsg=32636).geometry.iloc[0].area
149
  summary["alan_m2"] = float(area_m2)
150
  summary["alan_km2"] = float(area_m2 / 1_000_000)
151
- except Exception:
 
152
  summary["alan_m2"] = None
153
  summary["alan_km2"] = None
154
 
@@ -157,170 +171,386 @@ def summarize_pois(gdf, pois):
157
  return summary
158
 
159
  summary["toplam_poi"] = int(len(pois))
160
-
161
- for col in ["amenity", "leisure", "shop", "highway", "railway", "public_transport"]:
162
- if col in pois.columns:
163
- counts = pois[col].value_counts().to_dict()
164
- for k, v in counts.items():
165
- summary[f"{col}_{k}"] = int(v)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  return summary
167
 
168
 
169
  def build_poi_names_text(pois, max_per_category=15) -> str:
 
 
 
 
170
  if pois is None or len(pois) == 0:
171
  return "Bu mahalle için isim verisi olan POI bulunamadı.\n"
 
172
  if "name" not in pois.columns:
173
  return "Bu mahallede POI'ler için 'name' alanı bulunamadı.\n"
174
 
175
  lines = []
 
176
  def add_category(title, mask):
177
  sub = pois[mask]
178
- if sub is None or len(sub) == 0: return
179
- names = sub["name"].dropna().astype(str).str.strip()
 
 
 
 
 
 
180
  names = [n for n in names if n]
181
- if not names: return
 
182
  unique_names = sorted(set(names))[:max_per_category]
183
  lines.append(f"{title}:")
184
  for n in unique_names:
185
  lines.append(f" - {n}")
186
- lines.append("")
187
 
 
188
  if "amenity" in pois.columns:
189
- add_category("Okullar", pois["amenity"] == "school")
190
- add_category("Üniversiteler", pois["amenity"] == "university")
191
- add_category("Eczaneler", pois["amenity"] == "pharmacy")
192
- add_category("Kafeler", pois["amenity"] == "cafe")
193
- add_category("Restoranlar", pois["amenity"] == "restaurant")
194
 
195
  if "shop" in pois.columns:
196
- add_category("Marketler", pois["shop"].isin(["supermarket", "convenience", "mall"]))
 
 
197
 
198
  if not lines:
199
  return "Bu mahallede adı bilinen POI listesi çıkarılamadı.\n"
 
200
  return "\n".join(lines)
201
 
202
 
 
203
  def build_stats_text(summary: dict) -> str:
204
  if not summary:
205
  return "Veri bulunamadı."
206
-
207
  alan = summary.get("alan_km2", 0) or 0.0
208
  toplam_poi = summary.get("toplam_poi", 0)
209
  okul = summary.get("amenity_school", 0)
210
- univ = summary.get("amenity_university", 0)
211
  park = summary.get("leisure_park", 0)
212
  eczane = summary.get("amenity_pharmacy", 0)
213
  cafe = summary.get("amenity_cafe", 0)
214
  restoran = summary.get("amenity_restaurant", 0)
215
 
 
 
 
 
 
 
216
  lines = [
217
  f"- Tahmini alan: {alan:.2f} km²",
218
- f"- Toplam POI: {toplam_poi}",
219
  f"- Okul sayısı: {okul}",
220
- f"- Üniversite sayısı: {univ}",
221
  f"- Park sayısı: {park}",
222
  f"- Eczane sayısı: {eczane}",
223
  f"- Kafe sayısı: {cafe}",
224
  f"- Restoran sayısı: {restoran}",
 
 
 
 
 
225
  ]
 
226
  return "\n".join(lines)
227
 
228
 
229
- # ==========================================
230
- # OVERPASS VE HARİTA İŞLEMLERİ
231
- # ==========================================
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  def generate_overpass_query_from_llm(prompt: str, model_name: str) -> str:
 
 
 
 
233
  prompt = (prompt or "").strip()
234
  if not prompt:
235
  prompt = "Generate an Overpass QL query for my request."
236
 
237
  client = InferenceClient(model=model_name, token=HF_TOKEN if HF_TOKEN else None)
 
238
  messages = [
239
  {"role": "system", "content": OSM_QUERY_SYSTEM},
240
  {"role": "user", "content": prompt},
241
  ]
 
 
242
  result = client.chat_completion(
243
- messages=messages, max_tokens=400, temperature=0.2, top_p=0.9, stream=False
 
 
 
 
244
  )
 
 
245
  query_text = result.choices[0].message.content
 
 
246
  query_text = query_text.replace("```ql", "").replace("```QL", "").replace("```", "")
247
  return query_text.strip()
248
 
249
-
250
  def normalize_overpass_query(raw_query: str) -> str:
251
- if not raw_query: return ""
 
 
 
 
 
 
252
  q = raw_query.strip()
 
 
253
  if "Üretilen Overpass Sorgusu:" in q:
254
  q = q.split("Üretilen Overpass Sorgusu:", 1)[-1].strip()
 
 
255
  for token in ("```ql", "```QL", "```", "<code>", "</code>", "<pre>", "</pre>"):
256
  q = q.replace(token, "")
 
257
  return q.strip()
258
 
259
 
260
- def summarize_overpass_data(data: dict, max_examples: int = 30) -> str:
261
- if not data: return "Overpass sonucu boş."
262
- elements = data.get("elements", [])
263
- if not elements: return "Hiç element bulunamadı."
264
-
265
- total = len(elements)
266
- examples = []
267
- for e in elements[:max_examples]:
268
- etype = e.get("type")
269
- tags = e.get("tags", {})
270
- name = tags.get("name", "(isimsiz)")
271
- # Önemli etiketler
272
- tag_info = [f"{k}={v}" for k,v in tags.items() if k in ["amenity", "leisure", "shop", "highway"]]
273
- tag_str = ", ".join(tag_info) if tag_info else "etiket yok"
274
- examples.append(f"- {etype} | {name} | {tag_str}")
275
-
276
- return f"Toplam {total} öğe bulundu.\nÖrnekler:\n" + "\n".join(examples)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
 
278
 
279
  def add_poi_markers_to_map(pois, m, layer_prefix="POI"):
280
- if pois is None or len(pois) == 0: return
 
 
 
 
 
 
 
281
  try:
282
  if pois.crs is not None and pois.crs.to_epsg() != 4326:
283
  pois = pois.to_crs(epsg=4326)
284
- except Exception: pass
 
 
285
 
286
- layer_groups = {}
 
287
 
288
  for _, row in pois.iterrows():
289
  geom = row.geometry
290
- if geom is None or geom.is_empty: continue
291
-
 
 
292
  try:
293
  if geom.geom_type == "Point":
294
  lat, lon = geom.y, geom.x
295
  else:
296
  c = geom.centroid
297
  lat, lon = c.y, c.x
298
- except Exception: continue
 
299
 
300
  amenity = row.get("amenity")
301
- name = row.get("name")
302
-
303
- # Basit kategori
304
- cat = "Diğer"
305
- if isinstance(amenity, str): cat = f"Amenity: {amenity}"
306
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  layer_name = f"{layer_prefix} - {cat}"
 
308
  if layer_name not in layer_groups:
309
- fg = folium.FeatureGroup(name=layer_name, show=False) # Varsayılan kapalı olsun kalabalık olmasın
310
  fg.add_to(m)
311
  layer_groups[layer_name] = fg
312
-
313
- tooltip_txt = name if isinstance(name, str) else "POI"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  folium.CircleMarker(
315
- location=[lat, lon], radius=3, color="orange", fill=True, fill_opacity=0.6,
316
- tooltip=tooltip_txt, popup=f"{cat}: {tooltip_txt}"
 
 
 
 
 
317
  ).add_to(layer_groups[layer_name])
318
 
319
 
 
 
320
  def create_comparison_map(gdf1, gdf2):
321
  """
322
- İlk oluşturma anında kullanılan harita fonksiyonu.
 
323
  """
 
 
 
 
 
324
  centroid = None
325
  if gdf1 is not None and len(gdf1) > 0:
326
  centroid = gdf1.geometry.iloc[0].centroid
@@ -330,295 +560,551 @@ def create_comparison_map(gdf1, gdf2):
330
  if centroid is None:
331
  return "<b>Geometri bulunamadı.</b>"
332
 
333
- m = folium.Map(location=[centroid.y, centroid.x], zoom_start=13)
334
 
 
 
335
  if gdf1 is not None and len(gdf1) > 0:
336
  name1 = str(gdf1.get("neighborhood", ["Mahalle 1"]).iloc[0])
337
  folium.GeoJson(
338
  gdf1.geometry.__geo_interface__,
339
- name=f"{name1} Sınırı",
340
- style_function=lambda x: {"color": "red", "fill": False, "weight": 3}
 
 
 
 
341
  ).add_to(m)
 
 
342
  pois1 = get_pois_within(gdf1)
343
- add_poi_markers_to_map(pois1, m, layer_prefix=name1)
344
 
 
 
345
  if gdf2 is not None and len(gdf2) > 0:
346
  name2 = str(gdf2.get("neighborhood", ["Mahalle 2"]).iloc[0])
347
  folium.GeoJson(
348
  gdf2.geometry.__geo_interface__,
349
- name=f"{name2} Sınırı",
350
- style_function=lambda x: {"color": "blue", "fill": False, "weight": 3}
 
 
 
 
351
  ).add_to(m)
 
 
352
  pois2 = get_pois_within(gdf2)
353
- add_poi_markers_to_map(pois2, m, layer_prefix=name2)
354
 
355
  folium.LayerControl().add_to(m)
356
  return m._repr_html_()
357
 
358
 
359
- # ==========================================
360
- # ANA İŞ MANTIĞI (GÜNCELLENEN KISIMLAR)
361
- # ==========================================
362
-
363
- def prepare_comparison(city, district1, neigh1, district2, neigh2):
364
- """
365
- İki mahallenin verilerini hazırlar ve 'saved_data' olarak state döndürür.
366
- """
367
- city = (city or "").strip()
368
- district1 = (district1 or "").strip()
369
- neigh1 = (neigh1 or "").strip()
370
- district2 = (district2 or "").strip()
371
- neigh2 = (neigh2 or "").strip()
372
-
373
- if not city or not district1 or not neigh1 or not district2 or not neigh2:
374
- return "Eksik bilgi.", "Eksik bilgi.", "", "<b>Harita için yeterli veri yok.</b>", None
375
-
376
- # Mahalle 1
377
- gdf1 = get_neighborhood_gdf(city, district1, neigh1)
378
- stats1_txt = "Veri yok"
379
- labels1 = ""
380
- if gdf1 is not None and len(gdf1) > 0:
381
- pois1 = get_pois_within(gdf1)
382
- sum1 = summarize_pois(gdf1, pois1)
383
- stats1_txt = build_stats_text(sum1)
384
- labels1 = build_poi_names_text(pois1)
385
 
386
- # Mahalle 2
387
- gdf2 = get_neighborhood_gdf(city, district2, neigh2)
388
- stats2_txt = "Veri yok"
389
- labels2 = ""
390
- if gdf2 is not None and len(gdf2) > 0:
391
- pois2 = get_pois_within(gdf2)
392
- sum2 = summarize_pois(gdf2, pois2)
393
- stats2_txt = build_stats_text(sum2)
394
- labels2 = build_poi_names_text(pois2)
395
 
396
- # LLM Context
397
- context = (
398
- f"Şehir: {city}\n"
399
- f"1. Mahalle: {neigh1} ({district1})\n{stats1_txt}\nPOI İsimleri:\n{labels1}\n\n"
400
- f"2. Mahalle: {neigh2} ({district2})\n{stats2_txt}\nPOI İsimleri:\n{labels2}\n"
401
- )
402
 
403
- map_html = create_comparison_map(gdf1, gdf2)
404
-
405
- # <<< GÜNCELLENDİ: Koordinatları hesapla ve State içine kaydet
406
- center1, center2 = None, None
407
- if gdf1 is not None and len(gdf1) > 0:
408
- c = gdf1.to_crs(epsg=4326).geometry.iloc[0].centroid
409
- center1 = (c.y, c.x)
410
-
411
- if gdf2 is not None and len(gdf2) > 0:
412
- c = gdf2.to_crs(epsg=4326).geometry.iloc[0].centroid
413
- center2 = (c.y, c.x)
414
 
415
- saved_data = {
416
- "gdf1": gdf1, "gdf2": gdf2,
417
- "name1": neigh1, "name2": neigh2,
418
- "center1": center1, "center2": center2
419
- }
420
-
421
- return stats1_txt, stats2_txt, context, map_html, saved_data
422
-
423
-
424
- def run_overpass_to_map(query: str, saved_data=None):
425
  """
426
- Overpass sorgusunu çalıştırır ve saved_data varsa önceki sınırları da çizer.
 
427
  """
428
- query = normalize_overpass_query(query)
429
- if not query: return "<b>Sorgu boş.</b>", "Sorgu boş."
 
 
 
 
430
 
431
  try:
432
- resp = requests.post("https://overpass-api.de/api/interpreter", data={"data": query}, timeout=30)
433
  resp.raise_for_status()
434
  data = resp.json()
435
  except Exception as e:
436
- return f"<b>Hata:</b> {e}", "Hata oluştu."
 
 
 
 
 
437
 
438
  elements = data.get("elements", [])
439
-
440
- # Merkez belirleme
441
- center_lat, center_lon = None, None
442
- # 1. Öncelik: Kayıtlı mahalle
443
- if saved_data and saved_data.get("center1"):
444
- center_lat, center_lon = saved_data["center1"]
445
- # 2. Öncelik: Overpass verisi
446
- elif elements:
447
- el = elements[0]
448
- if "lat" in el: center_lat, center_lon = el["lat"], el["lon"]
449
- elif "geometry" in el: center_lat, center_lon = el["geometry"][0]["lat"], el["geometry"][0]["lon"]
450
 
 
 
 
 
 
 
451
  if center_lat is None:
452
- return "<b>Veri yok veya konum bulunamadı.</b>", "Veri yok."
453
-
454
- m = folium.Map(location=[center_lat, center_lon], zoom_start=13)
455
-
456
- # <<< GÜNCELLENDİ: Önce mahalle sınırlarını tekrar çiz
457
- if saved_data:
458
- gdf1 = saved_data.get("gdf1")
459
- if gdf1 is not None and len(gdf1) > 0:
460
- folium.GeoJson(
461
- gdf1.geometry.__geo_interface__,
462
- name=f"{saved_data.get('name1')} Sınırı",
463
- style_function=lambda x: {"color": "red", "fill": False, "weight": 2}
464
- ).add_to(m)
465
-
466
- gdf2 = saved_data.get("gdf2")
467
- if gdf2 is not None and len(gdf2) > 0:
468
- folium.GeoJson(
469
- gdf2.geometry.__geo_interface__,
470
- name=f"{saved_data.get('name2')} Sınırı",
471
- style_function=lambda x: {"color": "blue", "fill": False, "weight": 2}
472
- ).add_to(m)
473
-
474
- # Overpass sonuçlarını çiz
475
- fg = folium.FeatureGroup(name="Sorgu Sonuçları", show=True)
476
- fg.add_to(m)
477
 
478
  for el in elements:
 
479
  tags = el.get("tags", {})
480
- name = tags.get("name", el.get("type"))
481
- popup = f"{name}<br>" + "<br>".join([f"{k}:{v}" for k,v in tags.items() if k!='name'])
482
-
483
- if el["type"] == "node" and "lat" in el:
 
 
 
 
 
 
 
 
484
  folium.CircleMarker(
485
- [el["lat"], el["lon"]], radius=6, color="green", fill=True, popup=popup, tooltip=name
486
- ).add_to(fg)
487
- elif el["type"] == "way" and "geometry" in el:
488
- pts = [(p["lat"], p["lon"]) for p in el["geometry"]]
489
- folium.PolyLine(pts, color="green", weight=4, popup=popup, tooltip=name).add_to(fg)
 
 
 
 
 
 
 
 
 
 
 
 
 
490
 
491
  folium.LayerControl().add_to(m)
492
- return m._repr_html_(), summarize_overpass_data(data)
 
 
 
 
 
493
 
494
 
495
- def llm_overpass_to_map(natural_prompt, model_name, saved_data=None):
 
 
496
  """
497
- LLM'e koordinatları enjekte ederek sorgu üretir.
 
 
498
  """
499
- if not natural_prompt: return "Sorgu yok", "...", "..."
500
-
501
- # <<< GÜNCELLENDİ: Koordinatları prompt'a ekle
502
- coords_info = ""
503
- if saved_data:
504
- c1 = saved_data.get("center1")
505
- n1 = saved_data.get("name1")
506
- c2 = saved_data.get("center2")
507
- n2 = saved_data.get("name2")
508
- parts = []
509
- if c1: parts.append(f"{n1}: {c1[0]}, {c1[1]}")
510
- if c2: parts.append(f"{n2}: {c2[0]}, {c2[1]}")
511
- if parts:
512
- coords_info = "\nREFERENCE COORDINATES (Lat, Lon):\n" + "\n".join(parts)
513
-
514
- augmented_prompt = f"{natural_prompt}\n{coords_info}"
515
-
516
  try:
517
- query = generate_overpass_query_from_llm(augmented_prompt, model_name)
518
  except Exception as e:
519
- return f"LLM Hatası: {e}", "Hata", "Hata"
 
 
 
 
 
520
 
521
- map_html, summary = run_overpass_to_map(query, saved_data)
522
- return query, map_html, summary
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
523
 
524
 
525
- def respond(message, history, model_name, system_msg, max_tokens, temp, top_p, compare_ctx, spatial_ctx):
526
- # Eğer /osm ile başlıyorsa direkt sorgu moduna geçebilirdik ama UI'da ayrı buton var.
527
- # Burası normal sohbet.
528
- full_system = system_msg
529
- if compare_ctx:
530
- full_system += f"\n\nMAHALLE VERİLERİ:\n{compare_ctx}"
531
- if spatial_ctx:
532
- full_system += f"\n\nSON HARİTA SORGUSU SONUCU:\n{spatial_ctx}"
533
 
534
- client = InferenceClient(model=model_name, token=HF_TOKEN if HF_TOKEN else None)
535
- messages = [{"role": "system", "content": full_system}] + history + [{"role": "user", "content": message}]
536
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
537
  response = ""
538
- for chunk in client.chat_completion(messages=messages, max_tokens=max_tokens, stream=True, temperature=temp, top_p=top_p):
539
- if chunk.choices and chunk.choices[0].delta.content:
540
- response += chunk.choices[0].delta.content
541
- yield response
 
 
 
 
 
 
 
 
 
 
542
 
543
 
544
  # ==========================================
545
- # GRADIO UI
546
  # ==========================================
547
- with gr.Blocks(title="Mahalle Analiz Botu") as demo:
548
  gr.Markdown("## Mahalle Karşılaştırmalı Chat Botu")
549
 
550
- # State değişkenleri
551
  compare_state = gr.State("")
552
- spatial_state = gr.State("")
553
- gdf_state = gr.State(None) # <<< GÜNCELLENDİ: Mahalle geometrilerini tutar
554
 
555
  with gr.Row():
556
- # --- SOL: CHAT ---
557
- with gr.Column(scale=4):
558
- chatbox = gr.Chatbot(height=700, type="messages")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
559
  with gr.Row():
560
- model_sel = gr.Dropdown(
561
- ["abacusai/Dracarys-72B-Instruct", "meta-llama/Meta-Llama-3.1-8B-Instruct"],
562
- label="Model", value="abacusai/Dracarys-72B-Instruct"
 
 
563
  )
564
- sys_msg = gr.Textbox("Sen uzman bir şehir plancısısın.", label="Sistem Mesajı")
565
-
 
 
 
 
 
 
566
  with gr.Row():
567
- temp_sl = gr.Slider(0, 2, 0.7, label="Temperature")
568
- max_tok = gr.Slider(100, 2000, 512, label="Max Tokens")
569
-
570
- chat_iface = gr.ChatInterface(
571
- respond, chatbot=chatbox,
572
- additional_inputs=[model_sel, sys_msg, max_tok, temp_sl, gr.Slider(0,1,0.95), compare_state, spatial_state]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
  )
 
574
 
575
- # --- SAĞ: KONTROL VE HARİTA ---
576
- with gr.Column(scale=5):
577
- gr.Markdown("### 1. Mahalle Seçimi")
578
- city_in = gr.Textbox("Ankara", label="Şehir")
579
- with gr.Row():
580
- d1 = gr.Textbox("Gölbaşı", label="İlçe 1")
581
- n1 = gr.Textbox("İncek", label="Mahalle 1")
582
- with gr.Row():
583
- d2 = gr.Textbox("Gölbaşı", label="İlçe 2")
584
- n2 = gr.Textbox("Kızılcaşar", label="Mahalle 2")
585
-
586
- btn_compare = gr.Button("Karşılaştır", variant="primary")
587
-
588
- with gr.Row():
589
- stat1 = gr.Textbox(label="Mahalle 1 Bilgi", lines=4)
590
- stat2 = gr.Textbox(label="Mahalle 2 Bilgi", lines=4)
591
-
592
- map_out = gr.HTML(label="Harita", min_height=400)
593
-
594
- gr.Markdown("### 2. Akıllı Harita Sorgusu (LLM)")
595
- osm_prompt = gr.Textbox(label="Soru Sor (Örn: İncek'e en yakın üniversiteler)", lines=2)
596
- btn_gen_run = gr.Button("Sorgula ve Haritaya Ekle")
 
 
 
 
597
 
598
- ql_out = gr.Textbox(label="Oluşan Overpass Sorgusu", lines=3)
599
- btn_run_manual = gr.Button("Sorguyu Manuel Çalıştır")
600
-
601
- # --- EVENTLER ---
602
- # Karşılaştır butonuna basınca hem metinleri hem haritayı hem de GEOMETRİ STATE'ini güncelle
603
- btn_compare.click(
604
- prepare_comparison,
605
- inputs=[city_in, d1, n1, d2, n2],
606
- outputs=[stat1, stat2, compare_state, map_out, gdf_state]
607
- )
608
-
609
- # LLM ile sorgu üretirken GDF STATE'ini de gönderiyoruz (koordinatları bilsin diye)
610
- btn_gen_run.click(
611
- llm_overpass_to_map,
612
- inputs=[osm_prompt, model_sel, gdf_state],
613
- outputs=[ql_out, map_out, spatial_state]
614
- )
615
-
616
- # Manuel çalıştırmada da GDF STATE kullanıyoruz (eski sınırları çizsin diye)
617
- btn_run_manual.click(
618
- run_overpass_to_map,
619
- inputs=[ql_out, gdf_state],
620
- outputs=[map_out, spatial_state]
621
- )
622
 
623
  if __name__ == "__main__":
624
- demo.launch()
 
1
  import os
 
2
  import gradio as gr
3
  import geopandas as gpd
4
  import folium
5
  import requests
 
6
 
7
  # osmnx opsiyonel (fallback için), yoksa sorun değil
8
  try:
 
12
 
13
  from huggingface_hub import InferenceClient, login
14
 
15
+
16
  # ==========================================
17
  # HF TOKEN & MODEL
18
  # ==========================================
 
23
  else:
24
  print("UYARI: HF_TOKEN / HUGGINGFACE_HUB_TOKEN bulunamadı, gated modellere erişilemeyebilir.")
25
 
26
+ #client = InferenceClient(
27
+ # model="abacusai/Dracarys-72B-Instruct",
28
+ # token=HF_TOKEN if HF_TOKEN else None,
29
+ #)
30
+
31
+
32
+
33
+
34
  # ==========================================
35
  # ÖNCEDEN HAZIRLANMIŞ OSM VERİSİ
36
  # ==========================================
 
48
 
49
 
50
  DEFAULT_TAGS = {
51
+ "amenity": ["school", "pharmacy", "hospital", "restaurant", "cafe", "bank"],
52
  "leisure": ["park", "playground"],
53
  "shop": True,
54
+ # >>> ULAŞIM ETİKETLERİ
55
+ "highway": ["bus_stop"], # otobüs durakları
56
+ "railway": ["station", "halt", "tram_stop"], # tren / metro / tramvay istasyonları
57
+ "public_transport": ["stop_position", "platform"], # toplu taşıma durak/istasyonları
58
  }
59
 
 
 
 
 
 
 
 
 
 
 
60
 
 
 
 
 
 
 
61
 
62
  # ==========================================
63
  # OSM / CBS FONKSİYONLARI
64
  # ==========================================
65
  def get_neighborhood_gdf(city: str, district: str, neighborhood: str):
66
+ """
67
+ 1) Önce önceden kaydedilmiş GeoJSON'dan arar (city + district + neighborhood).
68
+ 2) Eğer orada yoksa ve osmnx mevcutsa, OSM'den canlı çeker (fallback).
69
+ """
70
  city = (city or "").strip()
71
  district = (district or "").strip()
72
  neighborhood = (neighborhood or "").strip()
 
84
 
85
  # 2) Fallback: canlı OSM çağrısı
86
  if ox is None:
87
+ print("OSM fallback kullanılamıyor: osmnx yüklü değil.")
88
  return None
89
 
90
+ # İlçe bilgisini de geocode sorgusuna ekliyoruz
91
  query = f"{neighborhood}, {district}, {city}, Türkiye"
92
+ print(f"OSM fallback: {query}")
93
+
94
  try:
95
  gdf_osm = ox.geocode_to_gdf(query)
96
  except Exception as e:
 
104
  gdf_osm["city"] = city
105
  gdf_osm["district"] = district
106
  gdf_osm["neighborhood"] = neighborhood
107
+
108
  return gdf_osm
109
 
110
 
111
+
112
  def get_pois_within(gdf, tags=None):
113
+ """
114
+ 1) Eğer GeoJSON'da bu mahalle için POI varsa, oradan döner.
115
+ 2) Yoksa ve osmnx mevcutsa, poligon üzerinden OSM'den canlı çeker (fallback).
116
+ """
117
  if gdf is None:
118
  return None
119
+
120
  if tags is None:
121
  tags = DEFAULT_TAGS
122
 
123
  # 1) Precomputed POI verisi
124
+ if (
125
+ pois_gdf is not None
126
+ and all(col in gdf.columns for col in ["city", "district", "neighborhood"])
127
+ ):
128
  row = gdf.iloc[0]
129
  city = row["city"]
130
  district = row["district"]
 
136
  & (pois_gdf["neighborhood"] == neighborhood)
137
  )
138
  pois_local = pois_gdf[mask]
139
+
140
  if pois_local is not None and len(pois_local) > 0:
141
  return pois_local
142
 
143
  # 2) Fallback: OSM'den canlı POI çek
144
  if ox is None:
145
+ print("OSM POI fallback kullanılamıyor: osmnx yüklü değil.")
146
  return None
147
 
148
  try:
 
156
 
157
  def summarize_pois(gdf, pois):
158
  summary = {}
159
+
160
  try:
161
  area_m2 = gdf.to_crs(epsg=32636).geometry.iloc[0].area
162
  summary["alan_m2"] = float(area_m2)
163
  summary["alan_km2"] = float(area_m2 / 1_000_000)
164
+ except Exception as e:
165
+ print("Alan hesaplama hatası:", e)
166
  summary["alan_m2"] = None
167
  summary["alan_km2"] = None
168
 
 
171
  return summary
172
 
173
  summary["toplam_poi"] = int(len(pois))
174
+
175
+ if "amenity" in pois.columns:
176
+ amenity_counts = pois["amenity"].value_counts().to_dict()
177
+ for k, v in amenity_counts.items():
178
+ summary[f"amenity_{k}"] = int(v)
179
+
180
+ if "leisure" in pois.columns:
181
+ leisure_counts = pois["leisure"].value_counts().to_dict()
182
+ for k, v in leisure_counts.items():
183
+ summary[f"leisure_{k}"] = int(v)
184
+
185
+ if "shop" in pois.columns:
186
+ shop_counts = pois["shop"].value_counts().to_dict()
187
+ for k, v in shop_counts.items():
188
+ summary[f"shop_{k}"] = int(v)
189
+
190
+ # >>> ULAŞIM: highway / railway / public_transport
191
+ if "highway" in pois.columns:
192
+ hw_counts = pois["highway"].value_counts().to_dict()
193
+ for k, v in hw_counts.items():
194
+ summary[f"highway_{k}"] = int(v)
195
+
196
+ if "railway" in pois.columns:
197
+ rw_counts = pois["railway"].value_counts().to_dict()
198
+ for k, v in rw_counts.items():
199
+ summary[f"railway_{k}"] = int(v)
200
+
201
+ if "public_transport" in pois.columns:
202
+ pt_counts = pois["public_transport"].value_counts().to_dict()
203
+ for k, v in pt_counts.items():
204
+ summary[f"public_transport_{k}"] = int(v)
205
+
206
  return summary
207
 
208
 
209
  def build_poi_names_text(pois, max_per_category=15) -> str:
210
+ """
211
+ POI GeoDataFrame'inden okul, eczane, kafe, restoran, market vb.
212
+ için isim listeleri çıkarır. LLM bağlamında kullanılacak metni döndürür.
213
+ """
214
  if pois is None or len(pois) == 0:
215
  return "Bu mahalle için isim verisi olan POI bulunamadı.\n"
216
+
217
  if "name" not in pois.columns:
218
  return "Bu mahallede POI'ler için 'name' alanı bulunamadı.\n"
219
 
220
  lines = []
221
+
222
  def add_category(title, mask):
223
  sub = pois[mask]
224
+ if sub is None or len(sub) == 0:
225
+ return
226
+ names = (
227
+ sub["name"]
228
+ .dropna()
229
+ .astype(str)
230
+ .str.strip()
231
+ )
232
  names = [n for n in names if n]
233
+ if not names:
234
+ return
235
  unique_names = sorted(set(names))[:max_per_category]
236
  lines.append(f"{title}:")
237
  for n in unique_names:
238
  lines.append(f" - {n}")
239
+ lines.append("") # kategori arası boş satır
240
 
241
+ # Kategoriler
242
  if "amenity" in pois.columns:
243
+ amenity = pois["amenity"]
244
+ add_category("Okullar", amenity == "school")
245
+ add_category("Eczaneler", amenity == "pharmacy")
246
+ add_category("Kafeler", amenity == "cafe")
247
+ add_category("Restoranlar", amenity == "restaurant")
248
 
249
  if "shop" in pois.columns:
250
+ shop = pois["shop"]
251
+ # İstersen buraya başka shop türleri de ekleyebilirsin
252
+ add_category("Marketler", shop.isin(["supermarket", "convenience", "mall", "department_store"]))
253
 
254
  if not lines:
255
  return "Bu mahallede adı bilinen POI listesi çıkarılamadı.\n"
256
+
257
  return "\n".join(lines)
258
 
259
 
260
+
261
  def build_stats_text(summary: dict) -> str:
262
  if not summary:
263
  return "Veri bulunamadı."
264
+
265
  alan = summary.get("alan_km2", 0) or 0.0
266
  toplam_poi = summary.get("toplam_poi", 0)
267
  okul = summary.get("amenity_school", 0)
 
268
  park = summary.get("leisure_park", 0)
269
  eczane = summary.get("amenity_pharmacy", 0)
270
  cafe = summary.get("amenity_cafe", 0)
271
  restoran = summary.get("amenity_restaurant", 0)
272
 
273
+ # >>> ULAŞIM SAYILARI
274
+ otobus_duragi = summary.get("highway_bus_stop", 0)
275
+ tren_istasyonu = summary.get("railway_station", 0) + summary.get("railway_halt", 0)
276
+ tramvay_duragi = summary.get("railway_tram_stop", 0)
277
+ pt_platform = summary.get("public_transport_platform", 0)
278
+
279
  lines = [
280
  f"- Tahmini alan: {alan:.2f} km²",
281
+ f"- Toplam POI (ilgi noktası): {toplam_poi}",
282
  f"- Okul sayısı: {okul}",
 
283
  f"- Park sayısı: {park}",
284
  f"- Eczane sayısı: {eczane}",
285
  f"- Kafe sayısı: {cafe}",
286
  f"- Restoran sayısı: {restoran}",
287
+ # >>> ULAŞIM SATIRLARI
288
+ f"- Otobüs durağı sayısı: {otobus_duragi}",
289
+ f"- Tren/metro istasyonu sayısı: {tren_istasyonu}",
290
+ f"- Tramvay durağı sayısı: {tramvay_duragi}",
291
+ f"- Toplu taşıma platform/istasyon öğesi: {pt_platform}",
292
  ]
293
+
294
  return "\n".join(lines)
295
 
296
 
297
+ import requests
298
+ import json
299
+ import geopandas as gpd
300
+ from shapely.geometry import shape
301
+
302
+ def is_osm_query(message: str) -> bool:
303
+ return isinstance(message, str) and message.strip().lower().startswith("/osm")
304
+
305
+ OSM_QUERY_SYSTEM = """
306
+ You are an expert in OpenStreetMap and Overpass API.
307
+ Produce only valid Overpass QL.
308
+ No explanations. No markdown.
309
+ Use [out:json][timeout:25]; at top.
310
+ End with: out center;
311
+ """
312
+
313
  def generate_overpass_query_from_llm(prompt: str, model_name: str) -> str:
314
+ """
315
+ Doğal dilde verilen prompt'u kullanarak Overpass QL sorgusu üretir.
316
+ Sadece geçerli Overpass QL döndürmeye çalışır, markdown vs. temizler.
317
+ """
318
  prompt = (prompt or "").strip()
319
  if not prompt:
320
  prompt = "Generate an Overpass QL query for my request."
321
 
322
  client = InferenceClient(model=model_name, token=HF_TOKEN if HF_TOKEN else None)
323
+
324
  messages = [
325
  {"role": "system", "content": OSM_QUERY_SYSTEM},
326
  {"role": "user", "content": prompt},
327
  ]
328
+
329
+ # Streaming'e gerek yok, tek seferde alalım
330
  result = client.chat_completion(
331
+ messages=messages,
332
+ max_tokens=400,
333
+ temperature=0.2,
334
+ top_p=0.9,
335
+ stream=False,
336
  )
337
+
338
+ # HF InferenceClient sonucu
339
  query_text = result.choices[0].message.content
340
+
341
+ # Olası ```ql ``` bloklarını temizle
342
  query_text = query_text.replace("```ql", "").replace("```QL", "").replace("```", "")
343
  return query_text.strip()
344
 
 
345
  def normalize_overpass_query(raw_query: str) -> str:
346
+ """
347
+ Kullanıcının yapıştırdığı sorgudan markdown / gereksiz metinleri temizler.
348
+ ```ql, ``` gibi blokları, 'Üretilen Overpass Sorgusu:' gibi başlıkları atar.
349
+ """
350
+ if not raw_query:
351
+ return ""
352
+
353
  q = raw_query.strip()
354
+
355
+ # Başlık metnini temizle
356
  if "Üretilen Overpass Sorgusu:" in q:
357
  q = q.split("Üretilen Overpass Sorgusu:", 1)[-1].strip()
358
+
359
+ # Markdown code fence'leri temizle
360
  for token in ("```ql", "```QL", "```", "<code>", "</code>", "<pre>", "</pre>"):
361
  q = q.replace(token, "")
362
+
363
  return q.strip()
364
 
365
 
366
+
367
+ # ==========================================
368
+ # MAHALLE KARŞILAŞTIRMA BAĞLAMI
369
+ # ==========================================
370
+ def prepare_comparison(city, district1, neigh1, district2, neigh2):
371
+ """
372
+ Butona basıldığında:
373
+ - Her iki mahalle için OSM özetini hazırlar
374
+ - İki metin döndürür
375
+ - LLM için karşılaştırma bağlamını üretir
376
+ """
377
+ city = (city or "").strip()
378
+ district1 = (district1 or "").strip()
379
+ neigh1 = (neigh1 or "").strip()
380
+ district2 = (district2 or "").strip()
381
+ neigh2 = (neigh2 or "").strip()
382
+
383
+ if not city or not district1 or not neigh1 or not district2 or not neigh2:
384
+ msg = "Şehir, ilçe ve iki mahalle de girilmelidir."
385
+ return (msg, msg, "", "<b>Harita için yeterli veri yok.</b>")
386
+
387
+ # Varsayılan olarak boş metinler
388
+ labels1 = "Bu mahalle için isim verisi çıkarılamadı."
389
+ labels2 = "Bu mahalle için isim verisi çıkarılamadı."
390
+
391
+ # Mahalle 1
392
+ gdf1 = get_neighborhood_gdf(city, district1, neigh1)
393
+ if gdf1 is None or len(gdf1) == 0:
394
+ stats1 = f"{city} / {district1} / {neigh1} için veri bulunamadı."
395
+ summary1 = None
396
+ else:
397
+ pois1 = get_pois_within(gdf1)
398
+ summary1 = summarize_pois(gdf1, pois1)
399
+ stats1 = build_stats_text(summary1)
400
+ # >>> POI isim metni
401
+ labels1 = build_poi_names_text(pois1)
402
+
403
+ # Mahalle 2
404
+ gdf2 = get_neighborhood_gdf(city, district2, neigh2)
405
+ if gdf2 is None or len(gdf2) == 0:
406
+ stats2 = f"{city} / {district2} / {neigh2} için veri bulunamadı."
407
+ summary2 = None
408
+ else:
409
+ pois2 = get_pois_within(gdf2)
410
+ summary2 = summarize_pois(gdf2, pois2)
411
+ stats2 = build_stats_text(summary2)
412
+ # >>> POI isim metni
413
+ labels2 = build_poi_names_text(pois2)
414
+
415
+ # LLM bağlamı
416
+ compare_context_parts = [
417
+ f"Şehir: {city}",
418
+ "",
419
+ f"1. Mahalle: {neigh1} (İlçe: {district1})",
420
+ stats1,
421
+ "",
422
+ "1. mahalledeki önemli POI isimleri (okullar, eczaneler, marketler, kafeler vb.):",
423
+ labels1,
424
+ "",
425
+ f"2. Mahalle: {neigh2} (İlçe: {district2})",
426
+ stats2,
427
+ "",
428
+ "2. mahalledeki önemli POI isimleri (okullar, eczaneler, marketler, kafeler vb.):",
429
+ labels2,
430
+ "",
431
+ "Bu iki mahalleyi alan, toplam POI sayısı, park, okul, kafe, restoran, eczane sayıları"
432
+ " ve verilen POI isimleri açısından karşılaştır."
433
+ " Kullanıcı soru sorarsa, hem sayısal verilere hem de POI isimlerine dayanarak"
434
+ " açıklayıcı ve dengeli bir karşılaştırma yap."
435
+ ]
436
+ compare_context = "\n".join(compare_context_parts)
437
+
438
+ # Harita HTML'i
439
+ map_html = create_comparison_map(gdf1, gdf2)
440
+
441
+ return stats1, stats2, compare_context, map_html
442
+
443
 
444
 
445
  def add_poi_markers_to_map(pois, m, layer_prefix="POI"):
446
+ """
447
+ POI GeoDataFrame'ini alır, amenity/leisure/shop/railway/highway/public_transport
448
+ sütunlarına göre kategorik katmanlar oluşturup haritaya ekler.
449
+ """
450
+ if pois is None or len(pois) == 0:
451
+ return
452
+
453
+ # Gerekirse WGS84'e (lat/lon) çevir
454
  try:
455
  if pois.crs is not None and pois.crs.to_epsg() != 4326:
456
  pois = pois.to_crs(epsg=4326)
457
+ except Exception:
458
+ # CRS yoksa veya hata olursa direkt devam
459
+ pass
460
 
461
+ # >>> ÖNEMLİ: layer_groups burada tanımlanmalı
462
+ layer_groups = {} # {kategori_ismi: folium.FeatureGroup}
463
 
464
  for _, row in pois.iterrows():
465
  geom = row.geometry
466
+ if geom is None or geom.is_empty:
467
+ continue
468
+
469
+ # Nokta değilse centroid al
470
  try:
471
  if geom.geom_type == "Point":
472
  lat, lon = geom.y, geom.x
473
  else:
474
  c = geom.centroid
475
  lat, lon = c.y, c.x
476
+ except Exception:
477
+ continue
478
 
479
  amenity = row.get("amenity")
480
+ leisure = row.get("leisure")
481
+ shop = row.get("shop")
482
+ highway = row.get("highway")
483
+ railway = row.get("railway")
484
+ public_transport = row.get("public_transport")
485
+
486
+ # Kategori belirle (öncelik: amenity > leisure > shop > railway > highway > public_transport)
487
+ if isinstance(amenity, str):
488
+ cat = f"Amenity: {amenity}"
489
+ elif isinstance(leisure, str):
490
+ cat = f"Leisure: {leisure}"
491
+ elif isinstance(shop, str):
492
+ cat = f"Shop: {shop}"
493
+ elif isinstance(railway, str):
494
+ cat = f"Railway: {railway}"
495
+ elif isinstance(highway, str):
496
+ cat = f"Highway: {highway}"
497
+ elif isinstance(public_transport, str):
498
+ cat = f"PT: {public_transport}"
499
+ else:
500
+ cat = "Diğer"
501
+
502
  layer_name = f"{layer_prefix} - {cat}"
503
+
504
  if layer_name not in layer_groups:
505
+ fg = folium.FeatureGroup(name=layer_name, show=True)
506
  fg.add_to(m)
507
  layer_groups[layer_name] = fg
508
+
509
+ name = row.get("name")
510
+
511
+ popup_items = []
512
+ # Önce isim
513
+ if isinstance(name, str) and name.strip():
514
+ popup_items.append(name.strip())
515
+
516
+ if isinstance(amenity, str):
517
+ popup_items.append(f"amenity={amenity}")
518
+ if isinstance(leisure, str):
519
+ popup_items.append(f"leisure={leisure}")
520
+ if isinstance(shop, str):
521
+ popup_items.append(f"shop={shop}")
522
+ if isinstance(railway, str):
523
+ popup_items.append(f"railway={railway}")
524
+ if isinstance(highway, str):
525
+ popup_items.append(f"highway={highway}")
526
+ if isinstance(public_transport, str):
527
+ popup_items.append(f"public_transport={public_transport}")
528
+
529
+ popup_text = ", ".join(popup_items) if popup_items else "POI"
530
+
531
  folium.CircleMarker(
532
+ location=[lat, lon],
533
+ radius=4,
534
+ popup=popup_text,
535
+ tooltip=name.strip() if isinstance(name, str) and name.strip() else None,
536
+ weight=1,
537
+ fill=True,
538
+ fill_opacity=0.7,
539
  ).add_to(layer_groups[layer_name])
540
 
541
 
542
+
543
+
544
  def create_comparison_map(gdf1, gdf2):
545
  """
546
+ İki mahalle poligonunu tek bir Folium haritasında gösterir.
547
+ + Her mahalle için POI'leri kategorik katmanlar hâlinde ekler.
548
  """
549
+ # Hiç veri yoksa
550
+ if (gdf1 is None or len(gdf1) == 0) and (gdf2 is None or len(gdf2) == 0):
551
+ return "<b>Harita için yeterli veri yok.</b>"
552
+
553
+ # Merkez olarak mevcut bir poligonun centroid'ini al
554
  centroid = None
555
  if gdf1 is not None and len(gdf1) > 0:
556
  centroid = gdf1.geometry.iloc[0].centroid
 
560
  if centroid is None:
561
  return "<b>Geometri bulunamadı.</b>"
562
 
563
+ m = folium.Map(location=[centroid.y, centroid.x], zoom_start=14)
564
 
565
+ # Mahalle 1
566
+ pois1 = None
567
  if gdf1 is not None and len(gdf1) > 0:
568
  name1 = str(gdf1.get("neighborhood", ["Mahalle 1"]).iloc[0])
569
  folium.GeoJson(
570
  gdf1.geometry.__geo_interface__,
571
+ name=name1,
572
+ style_function=lambda feat: {
573
+ "color": "red",
574
+ "fill": False,
575
+ "weight": 3,
576
+ },
577
  ).add_to(m)
578
+
579
+ # >>> Mahalle 1 için POI'ler
580
  pois1 = get_pois_within(gdf1)
581
+ add_poi_markers_to_map(pois1, m, layer_prefix=f"{name1} POI")
582
 
583
+ # Mahalle 2
584
+ pois2 = None
585
  if gdf2 is not None and len(gdf2) > 0:
586
  name2 = str(gdf2.get("neighborhood", ["Mahalle 2"]).iloc[0])
587
  folium.GeoJson(
588
  gdf2.geometry.__geo_interface__,
589
+ name=name2,
590
+ style_function=lambda feat: {
591
+ "color": "blue",
592
+ "fill": False,
593
+ "weight": 3,
594
+ },
595
  ).add_to(m)
596
+
597
+ # >>> Mahalle 2 için POI'ler
598
  pois2 = get_pois_within(gdf2)
599
+ add_poi_markers_to_map(pois2, m, layer_prefix=f"{name2} POI")
600
 
601
  folium.LayerControl().add_to(m)
602
  return m._repr_html_()
603
 
604
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
605
 
 
 
 
 
 
 
 
 
 
606
 
 
 
 
 
 
 
607
 
 
 
 
 
 
 
 
 
 
 
 
608
 
609
+ def run_overpass_to_map(query: str):
 
 
 
 
 
 
 
 
 
610
  """
611
+ Overpass QL sorgusunu çalıştırır, Folium haritası ve LLM bağlamı için
612
+ metinsel bir özet döndürür.
613
  """
614
+ query = normalize_overpass_query(query) if 'normalize_overpass_query' in globals() else query
615
+
616
+ if not query or not query.strip():
617
+ return "<b>Overpass sorgusu boş.</b>", "Geçerli bir Overpass sorgusu sağlanmadı."
618
+
619
+ url = "https://overpass-api.de/api/interpreter"
620
 
621
  try:
622
+ resp = requests.post(url, data={"data": query}, timeout=30)
623
  resp.raise_for_status()
624
  data = resp.json()
625
  except Exception as e:
626
+ print("Overpass isteği hatası:", e)
627
+ try:
628
+ print("Overpass response text:", resp.text[:500])
629
+ except Exception:
630
+ pass
631
+ return f"<b>Overpass isteği hatası:</b> {e}", "Overpass isteğinde hata oluştu, veri yok."
632
 
633
  elements = data.get("elements", [])
634
+ if not elements:
635
+ return "<b>Overpass sonucu: veri bulunamadı.</b>", "Overpass sonucu: hiç element bulunamadı."
 
 
 
 
 
 
 
 
 
636
 
637
+ # Merkez için ilk noktanın koordinatlarını bulalım
638
+ center_lat, center_lon = None, None
639
+ for el in elements:
640
+ if "lat" in el and "lon" in el:
641
+ center_lat, center_lon = el["lat"], el["lon"]
642
+ break
643
  if center_lat is None:
644
+ return "<b>Overpass sonucu: nokta verisi yok.</b>", "Overpass sonucu: nokta verisi bulunamadı."
645
+
646
+ m = folium.Map(location=[center_lat, center_lon], zoom_start=14)
647
+
648
+ # Katman: node'lar
649
+ fg_nodes = folium.FeatureGroup(name="Noktalar")
650
+ fg_nodes.add_to(m)
651
+
652
+ # Katman: yollar
653
+ fg_ways = folium.FeatureGroup(name="Yollar")
654
+ fg_ways.add_to(m)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
655
 
656
  for el in elements:
657
+ etype = el.get("type")
658
  tags = el.get("tags", {})
659
+ name = tags.get("name", "")
660
+ popup_items = []
661
+
662
+ if name:
663
+ popup_items.append(name)
664
+ for k, v in tags.items():
665
+ if k != "name":
666
+ popup_items.append(f"{k}={v}")
667
+ popup_text = "<br>".join(popup_items) if popup_items else etype
668
+
669
+ # Node → nokta işaretle
670
+ if etype == "node" and "lat" in el and "lon" in el:
671
  folium.CircleMarker(
672
+ location=[el["lat"], el["lon"]],
673
+ radius=4,
674
+ popup=popup_text,
675
+ tooltip=name or None,
676
+ weight=1,
677
+ fill=True,
678
+ fill_opacity=0.7,
679
+ ).add_to(fg_nodes)
680
+
681
+ # Way → polyline çiz
682
+ elif etype == "way" and "geometry" in el:
683
+ coords = [(p["lat"], p["lon"]) for p in el["geometry"]]
684
+ if len(coords) >= 2:
685
+ folium.PolyLine(
686
+ locations=coords,
687
+ popup=popup_text,
688
+ weight=3,
689
+ ).add_to(fg_ways)
690
 
691
  folium.LayerControl().add_to(m)
692
+
693
+ map_html = m._repr_html_()
694
+ summary_text = summarize_overpass_data(data)
695
+
696
+ return map_html, summary_text
697
+
698
 
699
 
700
+
701
+
702
+ def llm_overpass_to_map(natural_prompt: str, model_name: str):
703
  """
704
+ 1) Doğal dil prompt'tan LLM ile Overpass QL üretir
705
+ 2) Overpass QL'i çalıştırır, folium haritası ve özet döndürür
706
+ 3) Hem sorguyu, hem haritayı, hem de spatial özetini return eder
707
  """
708
+ if not natural_prompt or not natural_prompt.strip():
709
+ return (
710
+ "Doğal dil sorgu boş.",
711
+ "<b>Overpass sonucu: sorgu üretilemedi.</b>",
712
+ "Overpass sonucu: sorgu üretilemedi."
713
+ )
714
+
 
 
 
 
 
 
 
 
 
 
715
  try:
716
+ query = generate_overpass_query_from_llm(natural_prompt, model_name)
717
  except Exception as e:
718
+ print("LLM Overpass üretim hatası:", e)
719
+ return (
720
+ f"LLM Overpass üretim hatası: {e}",
721
+ "<b>Overpass sonucu: LLM hatası.</b>",
722
+ "Overpass sonucu: LLM hatası."
723
+ )
724
 
725
+ # run_overpass_to_map zaten (map_html, summary_text) döndürüyor
726
+ map_html, summary_text = run_overpass_to_map(query)
727
+
728
+ # 3 değer döndürüyoruz:
729
+ # - overpass_box içine yazılacak query
730
+ # - map_html (harita)
731
+ # - spatial_state (LLM için özet)
732
+ return query, map_html, summary_text
733
+
734
+
735
+ def summarize_overpass_data(data: dict, max_examples: int = 30) -> str:
736
+ """
737
+ Overpass JSON sonucundan LLM'e verilecek metinsel bir özet üretir.
738
+ Çok büyük verilerde token patlamaması için sınırlı örnek verir.
739
+ """
740
+ if not data:
741
+ return "Overpass sonucu boş veya geçersiz.\n"
742
+
743
+ elements = data.get("elements", [])
744
+ if not elements:
745
+ return "Overpass sonucu: hiç element bulunamadı.\n"
746
+
747
+ total_nodes = sum(1 for e in elements if e.get("type") == "node")
748
+ total_ways = sum(1 for e in elements if e.get("type") == "way")
749
+ total_rel = sum(1 for e in elements if e.get("type") == "relation")
750
+
751
+ # Basit tag istatistikleri
752
+ amenity_counts = {}
753
+ leisure_counts = {}
754
+ shop_counts = {}
755
+ highway_counts = {}
756
+ railway_counts = {}
757
+ pt_counts = {}
758
+
759
+ examples = []
760
+
761
+ for e in elements[:max_examples]:
762
+ etype = e.get("type")
763
+ tags = e.get("tags", {})
764
+ name = tags.get("name", "(isimsiz)")
765
+
766
+ amenity = tags.get("amenity")
767
+ leisure = tags.get("leisure")
768
+ shop = tags.get("shop")
769
+ highway = tags.get("highway")
770
+ railway = tags.get("railway")
771
+ pt = tags.get("public_transport")
772
+ opening_hours = tags.get("opening_hours")
773
+ wheelchair = tags.get("wheelchair")
774
+
775
+ if amenity:
776
+ amenity_counts[amenity] = amenity_counts.get(amenity, 0) + 1
777
+ if leisure:
778
+ leisure_counts[leisure] = leisure_counts.get(leisure, 0) + 1
779
+ if shop:
780
+ shop_counts[shop] = shop_counts.get(shop, 0) + 1
781
+ if highway:
782
+ highway_counts[highway] = highway_counts.get(highway, 0) + 1
783
+ if railway:
784
+ railway_counts[railway] = railway_counts.get(railway, 0) + 1
785
+ if pt:
786
+ pt_counts[pt] = pt_counts.get(pt, 0) + 1
787
+
788
+ # Örnek satır
789
+ tag_parts = []
790
+ for k in ["amenity", "leisure", "shop", "highway", "railway", "public_transport",
791
+ "opening_hours", "wheelchair"]:
792
+ v = tags.get(k)
793
+ if v:
794
+ tag_parts.append(f"{k}={v}")
795
+
796
+ tag_text = ", ".join(tag_parts) if tag_parts else "etiket yok"
797
+ examples.append(f"- {etype} | {name} | {tag_text}")
798
+
799
+ def dict_to_lines(title, d):
800
+ if not d:
801
+ return []
802
+ items = sorted(d.items(), key=lambda x: -x[1])
803
+ lines = [title]
804
+ for k, v in items:
805
+ lines.append(f" - {k}: {v}")
806
+ return lines
807
+
808
+ lines = [
809
+ f"Toplam node sayısı: {total_nodes}",
810
+ f"Toplam way sayısı: {total_ways}",
811
+ f"Toplam relation sayısı: {total_rel}",
812
+ "",
813
+ ]
814
+ lines += dict_to_lines("Amenity türleri:", amenity_counts)
815
+ lines += dict_to_lines("Leisure türleri:", leisure_counts)
816
+ lines += dict_to_lines("Shop türleri:", shop_counts)
817
+ lines += dict_to_lines("Highway türleri:", highway_counts)
818
+ lines += dict_to_lines("Railway türleri:", railway_counts)
819
+ lines += dict_to_lines("Public transport türleri:", pt_counts)
820
+
821
+ if examples:
822
+ lines.append("")
823
+ lines.append(f"İlk {len(examples)} elementten bazı örnekler:")
824
+ lines.extend(examples)
825
+
826
+ return "\n".join(lines)
827
+
828
+
829
+
830
+
831
+ # ==========================================
832
+ # LLM SOHBET FONKSİYONU
833
+ # ==========================================
834
+ def respond(
835
+ message,
836
+ history,
837
+ model_name,
838
+ system_message,
839
+ max_tokens,
840
+ temperature,
841
+ top_p,
842
+ compare_context, # mahalle karşılaştırma bağlamı
843
+ spatial_context, # 👈 yeni: son Overpass sonuç özeti
844
+ ):
845
+
846
+
847
+ # --------------- OSM SPATIAL QUERY MODU ---------------
848
+ # --- /osm ile başlayan mesajlar: sadece Overpass sorgusu üret ---
849
+ if is_osm_query(message):
850
+ user_text = message.lstrip()[4:].strip()
851
+ if not user_text:
852
+ user_text = "Generate an Overpass QL query for my request."
853
+
854
+ client = InferenceClient(model=model_name, token=HF_TOKEN if HF_TOKEN else None)
855
+
856
+ messages = [
857
+ {"role": "system", "content": OSM_QUERY_SYSTEM},
858
+ {"role": "user", "content": user_text},
859
+ ]
860
+
861
+ query_text = ""
862
+ for chunk in client.chat_completion(
863
+ messages=messages,
864
+ max_tokens=400,
865
+ stream=True,
866
+ temperature=0.2,
867
+ top_p=0.9,
868
+ ):
869
+ choices = chunk.choices
870
+ token_text = ""
871
+ if len(choices) and choices[0].delta.content:
872
+ token_text = choices[0].delta.content
873
+
874
+ query_text += token_text
875
+ # Kullanıcıya sadece sorguyu göster
876
+ yield f"Üretilen Overpass Sorgusu:\n```ql\n{query_text}\n```"
877
+
878
+
879
+ return
880
+ # --------------- NORMAL CHAT AKIŞI ---------------
881
 
882
 
 
 
 
 
 
 
 
 
883
 
 
 
884
 
885
+ """
886
+ Streaming chat using Hugging Face Inference API.
887
+ history: list of {"role": "...", "content": "..."}
888
+ """
889
+ # Temperature güvenli aralık (model max 2)
890
+ temperature = max(0.0, min(2.0, float(temperature)))
891
+ top_p = max(0.0, min(1.0, float(top_p)))
892
+
893
+ client = InferenceClient(model=model_name, token=HF_TOKEN if HF_TOKEN else None)
894
+
895
+ # System mesajına mahalle karşılaştırma bağlamını ekle
896
+ full_system = system_message
897
+ if compare_context:
898
+ full_system += (
899
+ "\n\nAşağıda aynı şehirdeki iki mahalleye ait sayısal özetler var.\n"
900
+ "Kullanıcı bu mahalleler hakkında soru sorarsa bu bağlama göre cevap ver:\n"
901
+ f"{compare_context}"
902
+ )
903
+
904
+ if spatial_context:
905
+ full_system += (
906
+ "\n\nAyrıca kullanıcı tarafından en son çalıştırılan bir Overpass (spatial) sorgusunun"
907
+ " özet sonuçları var. Kullanıcı bu sorgudan gelen veriler hakkında soru sorarsa,"
908
+ " bu özet bağlamına dayanarak cevap ver:\n"
909
+ f"{spatial_context}"
910
+ )
911
+
912
+ messages = [{"role": "system", "content": full_system}]
913
+ messages.extend(history)
914
+ messages.append({"role": "user", "content": message})
915
+
916
  response = ""
917
+ for chunk in client.chat_completion(
918
+ messages=messages,
919
+ max_tokens=max_tokens,
920
+ stream=True,
921
+ temperature=temperature,
922
+ top_p=top_p,
923
+ ):
924
+ choices = chunk.choices
925
+ token_text = ""
926
+ if len(choices) and choices[0].delta.content:
927
+ token_text = choices[0].delta.content
928
+
929
+ response += token_text
930
+ yield response + f"\n\n---\n**Model:** {model_name}"
931
 
932
 
933
  # ==========================================
934
+ # GRADIO ARAYÜZÜ (SOL CHAT, SAĞ KARŞILAŞTIRMA PANELİ)
935
  # ==========================================
936
+ with gr.Blocks() as demo:
937
  gr.Markdown("## Mahalle Karşılaştırmalı Chat Botu")
938
 
 
939
  compare_state = gr.State("")
940
+ spatial_state = gr.State("") # 👈 yeni: son Overpass sonuç özeti
941
+
942
 
943
  with gr.Row():
944
+ # SOL SÜTUN: CHAT
945
+ with gr.Column(scale=2):
946
+ chatbox = gr.Chatbot(height=800, scale=1)
947
+
948
+ model_dropdown = gr.Dropdown(
949
+ choices=[
950
+ # Küçük modeller
951
+ "google/gemma-2-2b-it", # 2B
952
+ "meta-llama/Meta-Llama-3.1-8B-Instruct", # 8B
953
+
954
+ # Büyük modeller
955
+ "abacusai/Dracarys-72B-Instruct", # 72B
956
+ "Qwen/Qwen2.5-72B-Instruct", # 72B
957
+
958
+ # Çok büyük model
959
+ "openai/gpt-oss-120b", # 120Bdı
960
+ ],
961
+ label="Model Seç (Bu listedeki modeller Hugging Face Inference API chat_completion ile uyumludur)",
962
+ value="abacusai/Dracarys-72B-Instruct"
963
+ )
964
+
965
+ system_box = gr.Textbox(
966
+ value="Sen şehir planlama ve mahalleler hakkında bilgi veren yardımsever bir asistansın.",
967
+ label="System message",
968
+ )
969
+
970
+ max_tokens_slider = gr.Slider(
971
+ minimum=1,
972
+ maximum=2048,
973
+ value=512,
974
+ step=1,
975
+ label="Max new tokens",
976
+ )
977
+
978
+ temperature_slider = gr.Slider(
979
+ minimum=0.0,
980
+ maximum=2.0, # model max 2
981
+ value=0.7,
982
+ step=0.1,
983
+ label="Temperature",
984
+ )
985
+
986
+ top_p_slider = gr.Slider(
987
+ minimum=0.1,
988
+ maximum=1.0,
989
+ value=0.95,
990
+ step=0.05,
991
+ label="Top-p (nucleus sampling)",
992
+ )
993
+
994
+ chatbot = gr.ChatInterface(
995
+ respond,
996
+ chatbot=chatbox,
997
+ type="messages",
998
+ title="Basit Chat Botu",
999
+ description="Küçük bir sohbet botu, HF Inference API ve OSM verisi ile çalışıyor.",
1000
+ additional_inputs=[
1001
+ model_dropdown,
1002
+ system_box,
1003
+ max_tokens_slider,
1004
+ temperature_slider,
1005
+ top_p_slider,
1006
+ compare_state, # >>> mahalle karşılaştırma bağlamı
1007
+ spatial_state, # >>> son Overpass sonucu özeti
1008
+
1009
+ ],
1010
+ )
1011
+
1012
+
1013
+ # SAĞ SÜTUN: MAHALLE KARŞILAŞTIRMA PANELİ
1014
+ with gr.Column(scale=1):
1015
+ gr.Markdown("### Mahalle Karşılaştırma")
1016
+
1017
+ city_in = gr.Textbox(
1018
+ label="Şehir",
1019
+ value="Ankara",
1020
+ placeholder="Örn: Ankara",
1021
+ )
1022
+
1023
+ # 1. mahalle: ilçe + mahalle aynı satırda
1024
  with gr.Row():
1025
+ district1_in = gr.Textbox(
1026
+ label="1. İlçe",
1027
+ value="Gölbaşı",
1028
+ scale=1,
1029
+ placeholder="Örn: Gölbaşı",
1030
  )
1031
+ neigh1_in = gr.Textbox(
1032
+ label="1. Mahalle",
1033
+ value="İncek",
1034
+ scale=2,
1035
+ placeholder="Örn: İncek Mahallesi",
1036
+ )
1037
+
1038
+ # 2. mahalle: ilçe + mahalle aynı satırda
1039
  with gr.Row():
1040
+ district2_in = gr.Textbox(
1041
+ label="2. İlçe",
1042
+ value="Gölbaşı",
1043
+ scale=1,
1044
+ placeholder="Örn: Gölbaşı",
1045
+ )
1046
+ neigh2_in = gr.Textbox(
1047
+ label="2. Mahalle",
1048
+ value="Kızılcaşar",
1049
+ scale=2,
1050
+ placeholder="Örn: Kızılcaşar Mahallesi",
1051
+ )
1052
+
1053
+ compare_btn = gr.Button("Karşılaştırmayı Hazırla")
1054
+
1055
+ stats1_box = gr.Textbox(
1056
+ label="1. Mahalle Özeti",
1057
+ lines=6,
1058
+ )
1059
+ stats2_box = gr.Textbox(
1060
+ label="2. Mahalle Özeti",
1061
+ lines=6,
1062
  )
1063
+ map_html = gr.HTML(label="Mahalle Haritası")
1064
 
1065
+ gr.Markdown("### Spatial Query (Overpass)")
1066
+
1067
+ # 1) LLM'e doğal dil prompt'u
1068
+ osm_nl_prompt = gr.Textbox(
1069
+ label="LLM ile Overpass Sorgusu (Doğal Dil)",
1070
+ lines=3,
1071
+ placeholder="Örn: İncek ve Kızılcaşar çevresindeki tüm park ve okulları getir"
1072
+ )
1073
+
1074
+ gen_and_run_btn = gr.Button("LLM ile Sorguyu Üret ve Çalıştır")
1075
+
1076
+ # 2) Üretilen veya manuel Overpass sorgusu
1077
+ overpass_box = gr.Textbox(
1078
+ label="Overpass Sorgusu",
1079
+ lines=6,
1080
+ placeholder="Buraya LLM'in ürettiği Overpass QL sorgusunu yapıştırın..."
1081
+ )
1082
+
1083
+ run_overpass_btn = gr.Button("Sorguyu Çalıştır ve Haritayı Güncelle")
1084
+
1085
+ # LLM ile üret + çalıştır
1086
+ gen_and_run_btn.click(
1087
+ fn=llm_overpass_to_map,
1088
+ inputs=[osm_nl_prompt, model_dropdown],
1089
+ outputs=[overpass_box, map_html, spatial_state], # 👈 artık spatial_state de güncelleniyor
1090
+ )
1091
 
1092
+
1093
+ # Manuel Overpass çalıştırma
1094
+ run_overpass_btn.click(
1095
+ fn=run_overpass_to_map,
1096
+ inputs=[overpass_box],
1097
+ outputs=[map_html, spatial_state], # 👈 harita + LLM için özet
1098
+ )
1099
+
1100
+
1101
+
1102
+ compare_btn.click(
1103
+ fn=prepare_comparison,
1104
+ inputs=[city_in, district1_in, neigh1_in, district2_in, neigh2_in],
1105
+ outputs=[stats1_box, stats2_box, compare_state, map_html],
1106
+ )
1107
+
 
 
 
 
 
 
 
 
1108
 
1109
  if __name__ == "__main__":
1110
+ demo.launch()