celikn's picture
Update app.py
ad18b49 verified
import os
import gradio as gr
import geopandas as gpd
import folium
import requests
# osmnx opsiyonel (fallback için), yoksa sorun değil
try:
import osmnx as ox
except ImportError:
ox = None
from huggingface_hub import InferenceClient, login
# ==========================================
# HF TOKEN & MODEL
# ==========================================
HF_TOKEN = (os.environ.get("HUGGINGFACE_HUB_TOKEN", "") or "").strip()
if HF_TOKEN:
login(token=HF_TOKEN)
else:
print("UYARI: HF_TOKEN / HUGGINGFACE_HUB_TOKEN bulunamadı, gated modellere erişilemeyebilir.")
#client = InferenceClient(
# model="abacusai/Dracarys-72B-Instruct",
# token=HF_TOKEN if HF_TOKEN else None,
#)
# ==========================================
# ÖNCEDEN HAZIRLANMIŞ OSM VERİSİ
# ==========================================
NEIGH_PATH = "data/neighborhoods.geojson"
POIS_PATH = "data/pois.geojson"
if not (os.path.exists(NEIGH_PATH) and os.path.exists(POIS_PATH)):
print("UYARI: data/ klasöründe neighborhoods.geojson / pois.geojson bulunamadı!")
neighborhoods_gdf = None
pois_gdf = None
else:
neighborhoods_gdf = gpd.read_file(NEIGH_PATH)
pois_gdf = gpd.read_file(POIS_PATH)
print(f"{len(neighborhoods_gdf)} mahalle/semt, {len(pois_gdf)} POI yüklendi.")
DEFAULT_TAGS = {
"amenity": ["school", "pharmacy", "hospital", "restaurant", "cafe", "bank"],
"leisure": ["park", "playground"],
"shop": True,
# >>> ULAŞIM ETİKETLERİ
"highway": ["bus_stop"], # otobüs durakları
"railway": ["station", "halt", "tram_stop"], # tren / metro / tramvay istasyonları
"public_transport": ["stop_position", "platform"], # toplu taşıma durak/istasyonları
}
# ==========================================
# OSM / CBS FONKSİYONLARI
# ==========================================
from shapely.geometry import Point
import math
def get_neighborhood_center(city, district, neighborhood):
"""
Verilen şehir/ilçe/mahalle için poligonu alır ve merkez (centroid) koordinatını döndürür.
(lat, lon) şeklinde WGS84 (EPSG:4326) döner.
"""
gdf = get_neighborhood_gdf(city, district, neighborhood)
if gdf is None or len(gdf) == 0:
return None
# Emin olmak için WGS84'e çevir
try:
gdf_ll = gdf.to_crs(epsg=4326)
except Exception:
gdf_ll = gdf # zaten 4326 ise
poly = gdf_ll.geometry.iloc[0]
c = poly.centroid
return c.y, c.x # (lat, lon)
def _build_nearest_poi_query(lat, lon, key, values, radius):
"""
Overpass için: verilen key + values (örn: amenity=university, shop=supermarket)
etrafında en yakın POI'yi arayan sorguyu üretir.
"""
if isinstance(values, str):
values = [values]
filters = []
for v in values:
filters.append(f' node["{key}"="{v}"](around:{radius},{lat},{lon});')
filters.append(f' way["{key}"="{v}"](around:{radius},{lat},{lon});')
filters.append(f' rel["{key}"="{v}"](around:{radius},{lat},{lon});')
filters_str = "\n".join(filters)
query = f"""
[out:json][timeout:25];
(
{filters_str}
);
out center 1;
"""
return query
def _haversine_m(lat1, lon1, lat2, lon2):
"""
İki nokta arasındaki yaklaşık mesafeyi (metre) hesaplar.
"""
R = 6371000 # Dünya yarıçapı (m)
phi1 = math.radians(lat1)
phi2 = math.radians(lat2)
dphi = math.radians(lat2 - lat1)
dlambda = math.radians(lon2 - lon1)
a = math.sin(dphi/2)**2 + math.cos(phi1)*math.cos(phi2)*math.sin(dlambda/2)**2
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
return R * c
def find_nearest_poi_overpass(city, district, neighborhood, key, values, radius=5000):
"""
Mahalle merkezinden belirli bir yarıçap içinde (m cinsinden)
verilen OSM tag'ine göre (örn: amenity, shop) en yakın POI'yi arar.
Örnek:
key="amenity", values=["university", "college"]
key="shop", values=["supermarket", "convenience"]
Dönüş:
None -> bulunamadı
dict -> {"name", "lat", "lon", "tag_key", "tag_value", "distance_m", "distance_km"}
"""
center = get_neighborhood_center(city, district, neighborhood)
if center is None:
return None
lat_c, lon_c = center
query = _build_nearest_poi_query(lat_c, lon_c, key, values, radius)
url = "https://overpass-api.de/api/interpreter"
try:
resp = requests.post(url, data={"data": query}, timeout=30)
resp.raise_for_status()
data = resp.json()
except Exception as e:
print("Overpass nearest POI isteği hatası:", e)
return None
elements = data.get("elements", [])
if not elements:
return None
# İlk element en yakın kabul ediliyor
e = elements[0]
tags = e.get("tags", {}) or {}
name = tags.get("name", "(isimsiz POI)")
# node ise lat/lon direk var; way/rel ise center altında
lat_p = e.get("lat") or (e.get("center") or {}).get("lat")
lon_p = e.get("lon") or (e.get("center") or {}).get("lon")
tag_value = None
if isinstance(values, list):
for v in values:
if tags.get(key) == v:
tag_value = v
break
if tag_value is None:
tag_value = tags.get(key)
else:
tag_value = tags.get(key)
dist_m = None
dist_km = None
if lat_p is not None and lon_p is not None:
dist_m = _haversine_m(lat_c, lon_c, lat_p, lon_p)
dist_km = dist_m / 1000.0
return {
"name": name,
"lat": lat_p,
"lon": lon_p,
"tag_key": key,
"tag_value": tag_value,
"distance_m": dist_m,
"distance_km": dist_km,
}
def answer_nearest_poi(city, district, neighborhood, key, values, human_label=None, radius=5000):
"""
Kullanıcıya gösterilecek generic cevap metni.
human_label: 'üniversite', 'market', 'hastane' gibi daha okunaklı isim.
"""
info = find_nearest_poi_overpass(city, district, neighborhood, key, values, radius=radius)
if info is None:
label = human_label or f"{key}={values}"
return f"{city} / {district} / {neighborhood} çevresinde belirli bir yarıçapta {label} bulunamadı."
name = info["name"]
lat = info["lat"]
lon = info["lon"]
dist_km = info["distance_km"]
label = human_label or info.get("tag_value") or f"{key}={values}"
if lat is not None and lon is not None and dist_km is not None:
return (
f"{city} / {district} / {neighborhood} mahallesi merkezine en yakın {label}: "
f"{name} (yaklaşık {dist_km:.2f} km, konum: {lat:.5f}, {lon:.5f})."
)
else:
return (
f"{city} / {district} / {neighborhood} mahallesi merkezine en yakın {label}: {name}."
)
def nearest_poi_wrapper(city, district, neighborhood, poi_type):
"""
Gradio dropdown'dan seçilen poi_type'ı gerçek tag'lere çeviren yardımcı.
"""
if poi_type == "Üniversite":
key = "amenity"
values = ["university", "college"]
label = "üniversite"
elif poi_type == "Market":
key = "shop"
values = ["supermarket", "convenience", "mall", "department_store"]
label = "market"
elif poi_type == "Hastane":
key = "amenity"
values = "hospital"
label = "hastane"
elif poi_type == "Eczane":
key = "amenity"
values = "pharmacy"
label = "eczane"
elif poi_type == "Park":
key = "leisure"
values = "park"
label = "park"
else:
# varsayılan: üniversite
key = "amenity"
values = ["university", "college"]
label = "üniversite"
return answer_nearest_poi(city, district, neighborhood, key, values, human_label=label)
def get_neighborhood_gdf(city: str, district: str, neighborhood: str):
"""
1) Önce önceden kaydedilmiş GeoJSON'dan arar (city + district + neighborhood).
2) Eğer orada yoksa ve osmnx mevcutsa, OSM'den canlı çeker (fallback).
"""
city = (city or "").strip()
district = (district or "").strip()
neighborhood = (neighborhood or "").strip()
# 1) Precomputed veri
if neighborhoods_gdf is not None:
mask = (
(neighborhoods_gdf["city"] == city)
& (neighborhoods_gdf["district"] == district)
& (neighborhoods_gdf["neighborhood"] == neighborhood)
)
gdf_local = neighborhoods_gdf[mask]
if gdf_local is not None and len(gdf_local) > 0:
return gdf_local
# 2) Fallback: canlı OSM çağrısı
if ox is None:
print("OSM fallback kullanılamıyor: osmnx yüklü değil.")
return None
# İlçe bilgisini de geocode sorgusuna ekliyoruz
query = f"{neighborhood}, {district}, {city}, Türkiye"
print(f"OSM fallback: {query}")
try:
gdf_osm = ox.geocode_to_gdf(query)
except Exception as e:
print("Mahalle geocode hatası (OSM fallback):", e)
return None
if gdf_osm is None or len(gdf_osm) == 0:
return None
gdf_osm = gdf_osm.copy()
gdf_osm["city"] = city
gdf_osm["district"] = district
gdf_osm["neighborhood"] = neighborhood
return gdf_osm
def get_pois_within(gdf, tags=None):
"""
1) Eğer GeoJSON'da bu mahalle için POI varsa, oradan döner.
2) Yoksa ve osmnx mevcutsa, poligon üzerinden OSM'den canlı çeker (fallback).
"""
if gdf is None:
return None
if tags is None:
tags = DEFAULT_TAGS
# 1) Precomputed POI verisi
if (
pois_gdf is not None
and all(col in gdf.columns for col in ["city", "district", "neighborhood"])
):
row = gdf.iloc[0]
city = row["city"]
district = row["district"]
neighborhood = row["neighborhood"]
mask = (
(pois_gdf["city"] == city)
& (pois_gdf["district"] == district)
& (pois_gdf["neighborhood"] == neighborhood)
)
pois_local = pois_gdf[mask]
if pois_local is not None and len(pois_local) > 0:
return pois_local
# 2) Fallback: OSM'den canlı POI çek
if ox is None:
print("OSM POI fallback kullanılamıyor: osmnx yüklü değil.")
return None
try:
polygon = gdf.geometry.iloc[0]
pois_osm = ox.features_from_polygon(polygon, tags)
return pois_osm
except Exception as e:
print("POI hatası (OSM fallback):", e)
return None
def summarize_pois(gdf, pois):
summary = {}
try:
area_m2 = gdf.to_crs(epsg=32636).geometry.iloc[0].area
summary["alan_m2"] = float(area_m2)
summary["alan_km2"] = float(area_m2 / 1_000_000)
except Exception as e:
print("Alan hesaplama hatası:", e)
summary["alan_m2"] = None
summary["alan_km2"] = None
if pois is None or len(pois) == 0:
summary["toplam_poi"] = 0
return summary
summary["toplam_poi"] = int(len(pois))
if "amenity" in pois.columns:
amenity_counts = pois["amenity"].value_counts().to_dict()
for k, v in amenity_counts.items():
summary[f"amenity_{k}"] = int(v)
if "leisure" in pois.columns:
leisure_counts = pois["leisure"].value_counts().to_dict()
for k, v in leisure_counts.items():
summary[f"leisure_{k}"] = int(v)
if "shop" in pois.columns:
shop_counts = pois["shop"].value_counts().to_dict()
for k, v in shop_counts.items():
summary[f"shop_{k}"] = int(v)
# >>> ULAŞIM: highway / railway / public_transport
if "highway" in pois.columns:
hw_counts = pois["highway"].value_counts().to_dict()
for k, v in hw_counts.items():
summary[f"highway_{k}"] = int(v)
if "railway" in pois.columns:
rw_counts = pois["railway"].value_counts().to_dict()
for k, v in rw_counts.items():
summary[f"railway_{k}"] = int(v)
if "public_transport" in pois.columns:
pt_counts = pois["public_transport"].value_counts().to_dict()
for k, v in pt_counts.items():
summary[f"public_transport_{k}"] = int(v)
return summary
def build_poi_names_text(pois, max_per_category=15) -> str:
"""
POI GeoDataFrame'inden okul, eczane, kafe, restoran, market vb.
için isim listeleri çıkarır. LLM bağlamında kullanılacak metni döndürür.
"""
if pois is None or len(pois) == 0:
return "Bu mahalle için isim verisi olan POI bulunamadı.\n"
if "name" not in pois.columns:
return "Bu mahallede POI'ler için 'name' alanı bulunamadı.\n"
lines = []
def add_category(title, mask):
sub = pois[mask]
if sub is None or len(sub) == 0:
return
names = (
sub["name"]
.dropna()
.astype(str)
.str.strip()
)
names = [n for n in names if n]
if not names:
return
unique_names = sorted(set(names))[:max_per_category]
lines.append(f"{title}:")
for n in unique_names:
lines.append(f" - {n}")
lines.append("") # kategori arası boş satır
# Kategoriler
if "amenity" in pois.columns:
amenity = pois["amenity"]
add_category("Okullar", amenity == "school")
add_category("Eczaneler", amenity == "pharmacy")
add_category("Kafeler", amenity == "cafe")
add_category("Restoranlar", amenity == "restaurant")
if "shop" in pois.columns:
shop = pois["shop"]
# İstersen buraya başka shop türleri de ekleyebilirsin
add_category("Marketler", shop.isin(["supermarket", "convenience", "mall", "department_store"]))
if not lines:
return "Bu mahallede adı bilinen POI listesi çıkarılamadı.\n"
return "\n".join(lines)
def build_stats_text(summary: dict) -> str:
if not summary:
return "Veri bulunamadı."
alan = summary.get("alan_km2", 0) or 0.0
toplam_poi = summary.get("toplam_poi", 0)
okul = summary.get("amenity_school", 0)
park = summary.get("leisure_park", 0)
eczane = summary.get("amenity_pharmacy", 0)
cafe = summary.get("amenity_cafe", 0)
restoran = summary.get("amenity_restaurant", 0)
# >>> ULAŞIM SAYILARI
otobus_duragi = summary.get("highway_bus_stop", 0)
tren_istasyonu = summary.get("railway_station", 0) + summary.get("railway_halt", 0)
tramvay_duragi = summary.get("railway_tram_stop", 0)
pt_platform = summary.get("public_transport_platform", 0)
lines = [
f"- Tahmini alan: {alan:.2f} km²",
f"- Toplam POI (ilgi noktası): {toplam_poi}",
f"- Okul sayısı: {okul}",
f"- Park sayısı: {park}",
f"- Eczane sayısı: {eczane}",
f"- Kafe sayısı: {cafe}",
f"- Restoran sayısı: {restoran}",
# >>> ULAŞIM SATIRLARI
f"- Otobüs durağı sayısı: {otobus_duragi}",
f"- Tren/metro istasyonu sayısı: {tren_istasyonu}",
f"- Tramvay durağı sayısı: {tramvay_duragi}",
f"- Toplu taşıma platform/istasyon öğesi: {pt_platform}",
]
return "\n".join(lines)
import requests
import json
import geopandas as gpd
from shapely.geometry import shape
from pathlib import Path
def is_osm_query(message: str) -> bool:
return isinstance(message, str) and message.strip().lower().startswith("/osm")
OSM_PROMPT_PATH = Path("osm_query_system.txt") # veya Path("data/osm_query_system.txt")
try:
with OSM_PROMPT_PATH.open("r", encoding="utf-8") as f:
OSM_QUERY_SYSTEM = f.read()
print(f"OSM system prompt '{OSM_PROMPT_PATH}' dosyasından yüklendi.")
except FileNotFoundError:
print(f"UYARI: {OSM_PROMPT_PATH} bulunamadı, kısa yedek prompt kullanılacak.")
OSM_QUERY_SYSTEM = """
You are an expert in OpenStreetMap and Overpass API.
Produce only valid Overpass QL.
No explanations. No markdown.
Use [out:json][timeout:25]; at top.
End with: out center;
"""
def generate_overpass_query_from_llm(prompt: str, model_name: str) -> str:
"""
Doğal dilde verilen prompt'u kullanarak Overpass QL sorgusu üretir.
Sadece geçerli Overpass QL döndürmeye çalışır, markdown vs. temizler.
"""
prompt = (prompt or "").strip()
if not prompt:
prompt = "Generate an Overpass QL query for my request."
client = InferenceClient(model=model_name, token=HF_TOKEN if HF_TOKEN else None)
messages = [
{"role": "system", "content": OSM_QUERY_SYSTEM},
{"role": "user", "content": prompt},
]
# Streaming'e gerek yok, tek seferde alalım
result = client.chat_completion(
messages=messages,
max_tokens=400,
temperature=0.2,
top_p=0.9,
stream=False,
)
# HF InferenceClient sonucu
query_text = result.choices[0].message.content
# Olası ```ql ``` bloklarını temizle
query_text = query_text.replace("```ql", "").replace("```QL", "").replace("```", "")
return query_text.strip()
def normalize_overpass_query(raw_query: str) -> str:
if not raw_query:
return ""
q = raw_query.strip()
if "{{geocodeArea:" in q:
# Kullanıcıyı uyarmak için
raise ValueError("Overpass API, {{geocodeArea:...}} makrosunu desteklemez. "
"Lütfen bunun yerine area[...] filtresi veya numeric area id kullanın.")
# (senin mevcut temizleme kodların)
for token in ("```ql", "```QL", "```", "<code>", "</code>", "<pre>", "</pre>"):
q = q.replace(token, "")
return q.strip()
# ==========================================
# MAHALLE KARŞILAŞTIRMA BAĞLAMI
# ==========================================
def prepare_comparison(city1, district1, neigh1,city2, district2, neigh2):
"""
Butona basıldığında:
- Her iki mahalle için OSM özetini hazırlar
- İki metin döndürür
- LLM için karşılaştırma bağlamını üretir
"""
city1 = (city1 or "").strip()
district1 = (district1 or "").strip()
neigh1 = (neigh1 or "").strip()
city2 = (city2 or "").strip()
district2 = (district2 or "").strip()
neigh2 = (neigh2 or "").strip()
if not city1 or not district1 or not city1 or not neigh1 or not district2 or not neigh2:
msg = "Şehir, ilçe ve iki mahalle de girilmelidir."
return (msg, msg, "", "<b>Harita için yeterli veri yok.</b>")
# Varsayılan olarak boş metinler
labels1 = "Bu mahalle için isim verisi çıkarılamadı."
labels2 = "Bu mahalle için isim verisi çıkarılamadı."
# Mahalle 1
gdf1 = get_neighborhood_gdf(city1, district1, neigh1)
if gdf1 is None or len(gdf1) == 0:
stats1 = f"{city1} / {district1} / {neigh1} için veri bulunamadı."
summary1 = None
else:
pois1 = get_pois_within(gdf1)
summary1 = summarize_pois(gdf1, pois1)
stats1 = build_stats_text(summary1)
# >>> POI isim metni
labels1 = build_poi_names_text(pois1)
# Mahalle 2
gdf2 = get_neighborhood_gdf(city2, district2, neigh2)
if gdf2 is None or len(gdf2) == 0:
stats2 = f"{city2} / {district2} / {neigh2} için veri bulunamadı."
summary2 = None
else:
pois2 = get_pois_within(gdf2)
summary2 = summarize_pois(gdf2, pois2)
stats2 = build_stats_text(summary2)
# >>> POI isim metni
labels2 = build_poi_names_text(pois2)
# LLM bağlamı
compare_context_parts = [
f"Şehir: {city1}",
"",
f"1. Mahalle: {neigh1} (İlçe: {district1})",
stats1,
"",
"1. mahalledeki önemli POI isimleri (okullar, eczaneler, marketler, kafeler vb.):",
labels1,
"",
f"Şehir: {city2}",
"",
f"2. Mahalle: {neigh2} (İlçe: {district2})",
stats2,
"",
"2. mahalledeki önemli POI isimleri (okullar, eczaneler, marketler, kafeler vb.):",
labels2,
"",
"Bu iki mahalleyi alan, toplam POI sayısı, park, okul, kafe, restoran, eczane sayıları"
" ve verilen POI isimleri açısından karşılaştır."
" Kullanıcı soru sorarsa, hem sayısal verilere hem de POI isimlerine dayanarak"
" açıklayıcı ve dengeli bir karşılaştırma yap."
]
compare_context = "\n".join(compare_context_parts)
# Harita HTML'i
map_html = create_comparison_map(gdf1, gdf2)
return stats1, stats2, compare_context, map_html,gdf1, gdf2
def add_poi_markers_to_map(pois, m, layer_prefix="POI"):
"""
POI GeoDataFrame'ini alır, amenity/leisure/shop/railway/highway/public_transport
sütunlarına göre kategorik katmanlar oluşturup haritaya ekler.
"""
if pois is None or len(pois) == 0:
return
# Gerekirse WGS84'e (lat/lon) çevir
try:
if pois.crs is not None and pois.crs.to_epsg() != 4326:
pois = pois.to_crs(epsg=4326)
except Exception:
# CRS yoksa veya hata olursa direkt devam
pass
# >>> ÖNEMLİ: layer_groups burada tanımlanmalı
layer_groups = {} # {kategori_ismi: folium.FeatureGroup}
for _, row in pois.iterrows():
geom = row.geometry
if geom is None or geom.is_empty:
continue
# Nokta değilse centroid al
try:
if geom.geom_type == "Point":
lat, lon = geom.y, geom.x
else:
c = geom.centroid
lat, lon = c.y, c.x
except Exception:
continue
amenity = row.get("amenity")
leisure = row.get("leisure")
shop = row.get("shop")
highway = row.get("highway")
railway = row.get("railway")
public_transport = row.get("public_transport")
# Kategori belirle (öncelik: amenity > leisure > shop > railway > highway > public_transport)
if isinstance(amenity, str):
cat = f"Amenity: {amenity}"
elif isinstance(leisure, str):
cat = f"Leisure: {leisure}"
elif isinstance(shop, str):
cat = f"Shop: {shop}"
elif isinstance(railway, str):
cat = f"Railway: {railway}"
elif isinstance(highway, str):
cat = f"Highway: {highway}"
elif isinstance(public_transport, str):
cat = f"PT: {public_transport}"
else:
cat = "Diğer"
layer_name = f"{layer_prefix} - {cat}"
if layer_name not in layer_groups:
fg = folium.FeatureGroup(name=layer_name, show=True)
fg.add_to(m)
layer_groups[layer_name] = fg
name = row.get("name")
popup_items = []
# Önce isim
if isinstance(name, str) and name.strip():
popup_items.append(name.strip())
if isinstance(amenity, str):
popup_items.append(f"amenity={amenity}")
if isinstance(leisure, str):
popup_items.append(f"leisure={leisure}")
if isinstance(shop, str):
popup_items.append(f"shop={shop}")
if isinstance(railway, str):
popup_items.append(f"railway={railway}")
if isinstance(highway, str):
popup_items.append(f"highway={highway}")
if isinstance(public_transport, str):
popup_items.append(f"public_transport={public_transport}")
popup_text = ", ".join(popup_items) if popup_items else "POI"
folium.CircleMarker(
location=[lat, lon],
radius=4,
popup=popup_text,
tooltip=name.strip() if isinstance(name, str) and name.strip() else None,
weight=1,
fill=True,
fill_opacity=0.7,
).add_to(layer_groups[layer_name])
def create_comparison_map(gdf1, gdf2):
"""
İki mahalle poligonunu tek bir Folium haritasında gösterir.
+ Her mahalle için POI'leri kategorik katmanlar hâlinde ekler.
"""
# Hiç veri yoksa
if (gdf1 is None or len(gdf1) == 0) and (gdf2 is None or len(gdf2) == 0):
return "<b>Harita için yeterli veri yok.</b>"
# Merkez olarak mevcut bir poligonun centroid'ini al
centroid = None
if gdf1 is not None and len(gdf1) > 0:
centroid = gdf1.geometry.iloc[0].centroid
elif gdf2 is not None and len(gdf2) > 0:
centroid = gdf2.geometry.iloc[0].centroid
if centroid is None:
return "<b>Geometri bulunamadı.</b>"
m = folium.Map(location=[centroid.y, centroid.x], zoom_start=14)
# Mahalle 1
pois1 = None
if gdf1 is not None and len(gdf1) > 0:
name1 = str(gdf1.get("neighborhood", ["Mahalle 1"]).iloc[0])
folium.GeoJson(
gdf1.geometry.__geo_interface__,
name=name1,
style_function=lambda feat: {
"color": "red",
"fill": False,
"weight": 3,
},
).add_to(m)
# >>> Mahalle 1 için POI'ler
pois1 = get_pois_within(gdf1)
add_poi_markers_to_map(pois1, m, layer_prefix=f"{name1} POI")
# Mahalle 2
pois2 = None
if gdf2 is not None and len(gdf2) > 0:
name2 = str(gdf2.get("neighborhood", ["Mahalle 2"]).iloc[0])
folium.GeoJson(
gdf2.geometry.__geo_interface__,
name=name2,
style_function=lambda feat: {
"color": "blue",
"fill": False,
"weight": 3,
},
).add_to(m)
# >>> Mahalle 2 için POI'ler
pois2 = get_pois_within(gdf2)
add_poi_markers_to_map(pois2, m, layer_prefix=f"{name2} POI")
folium.LayerControl().add_to(m)
return m._repr_html_()
def run_overpass_to_map(query: str,
previous_elements: list | None,
previous_spatial: str | None,
layer_color: str,
base_gdf1=None,
base_gdf2=None):
layer_name = "Spatial Query"
query = normalize_overpass_query(query) if 'normalize_overpass_query' in globals() else query
if not query or not query.strip():
return (
"<b>Overpass sorgusu boş.</b>",
previous_spatial or "Geçerli bir Overpass sorgusu sağlanmadı.",
previous_elements,
)
url = "https://overpass-api.de/api/interpreter"
try:
resp = requests.post(url, data={"data": query}, timeout=30)
resp.raise_for_status()
data = resp.json()
except Exception as e:
print("Overpass isteği hatası:", e)
try:
print("Overpass response text:", resp.text[:500])
except Exception:
pass
return (
f"<b>Overpass isteği hatası:</b> {e}",
previous_spatial or "Overpass isteğinde hata oluştu, veri yok.",
previous_elements,
)
new_elements = data.get("elements", [])
if not new_elements:
return (
"<b>Overpass sonucu: veri bulunamadı.</b>",
previous_spatial or "Overpass sonucu: hiç element bulunamadı.",
previous_elements,
)
# ---- Eski + yeni elementleri birleştir ----
if not previous_elements:
all_elements = new_elements
else:
all_elements = previous_elements + new_elements
# ---- Merkez bul (tüm elementler üzerinden) ----
center_lat, center_lon = None, None
for el in all_elements:
if "lat" in el and "lon" in el:
center_lat, center_lon = el["lat"], el["lon"]
break
if center_lat is None:
return (
"<b>Overpass sonucu: nokta verisi yok.</b>",
previous_spatial or "Overpass sonucu: nokta verisi bulunamadı.",
all_elements,
)
# ---- Haritayı çiz ----
# Önce merkez: mümkünse mahallelerden, yoksa Overpass noktalardan
if base_gdf1 is not None and len(base_gdf1) > 0:
c = base_gdf1.geometry.iloc[0].centroid
center_lat, center_lon = c.y, c.x
elif base_gdf2 is not None and len(base_gdf2) > 0:
c = base_gdf2.geometry.iloc[0].centroid
center_lat, center_lon = c.y, c.x
# (aksi halde yukarıda all_elements'tan zaten bulduk)
m = folium.Map(location=[center_lat, center_lon], zoom_start=14)
# 1. mahalleyi tekrar çiz
if base_gdf1 is not None and len(base_gdf1) > 0:
name1 = str(base_gdf1.get("neighborhood", ["Mahalle 1"]).iloc[0])
folium.GeoJson(
base_gdf1.geometry.__geo_interface__,
name=name1,
style_function=lambda feat: {
"color": "red",
"fill": False,
"weight": 3,
},
).add_to(m)
pois1 = get_pois_within(base_gdf1)
add_poi_markers_to_map(pois1, m, layer_prefix=f"{name1} POI")
# 2. mahalleyi tekrar çiz
if base_gdf2 is not None and len(base_gdf2) > 0:
name2 = str(base_gdf2.get("neighborhood", ["Mahalle 2"]).iloc[0])
folium.GeoJson(
base_gdf2.geometry.__geo_interface__,
name=name2,
style_function=lambda feat: {
"color": "blue",
"fill": False,
"weight": 3,
},
).add_to(m)
pois2 = get_pois_within(base_gdf2)
add_poi_markers_to_map(pois2, m, layer_prefix=f"{name2} POI")
# Burada tek bir "spatial" katman oluşturuyoruz
fg_nodes = folium.FeatureGroup(
name=f"{layer_name} - Noktalar", # katman adı
show=True
)
fg_nodes.add_to(m)
fg_ways = folium.FeatureGroup(
name=f"{layer_name} - Yollar",
show=True
)
fg_ways.add_to(m)
for el in all_elements:
etype = el.get("type")
tags = el.get("tags", {}) or {}
name = tags.get("name", "")
popup_items = []
if name:
popup_items.append(name)
for k, v in tags.items():
if k != "name":
popup_items.append(f"{k}={v}")
popup_text = "<br>".join(popup_items) if popup_items else etype
if etype == "node" and "lat" in el and "lon" in el:
folium.CircleMarker(
location=[el["lat"], el["lon"]],
radius=4,
popup=popup_text,
tooltip=name or None,
weight=1,
fill=True,
fill_opacity=0.7,
color=layer_color, # 👈 stroke rengi
fill_color=layer_color, # 👈 doldurma rengi
).add_to(fg_nodes)
elif etype == "way" and "geometry" in el:
coords = [(p["lat"], p["lon"]) for p in el["geometry"]]
if len(coords) >= 2:
folium.PolyLine(
locations=coords,
popup=popup_text,
weight=3,
color=layer_color, # 👈 yol rengi
).add_to(fg_ways)
folium.LayerControl().add_to(m)
map_html = m._repr_html_()
# ---- Yeni spatial özet ----
new_summary = summarize_overpass_data({"elements": new_elements})
if previous_spatial:
combined_spatial = previous_spatial + "\n\n--- Yeni Sorgu ---\n" + new_summary
else:
combined_spatial = new_summary
return map_html, combined_spatial, all_elements
def llm_overpass_to_map(natural_prompt: str,
model_name: str,
previous_elements: list | None,
previous_spatial: str | None,
layer_color: str,
base_gdf1=None,
base_gdf2=None):
if not natural_prompt or not natural_prompt.strip():
return (
"Doğal dil sorgu boş.",
"<b>Overpass sonucu: sorgu üretilemedi.</b>",
previous_spatial or "Overpass sonucu: sorgu üretilemedi.",
previous_elements,
)
try:
query = generate_overpass_query_from_llm(natural_prompt, model_name)
except Exception as e:
print("LLM Overpass üretim hatası:", e)
return (
f"LLM Overpass üretim hatası: {e}",
"<b>Overpass sonucu: LLM hatası.</b>",
previous_spatial or "Overpass sonucu: LLM hatası.",
previous_elements,
)
map_html, combined_spatial, all_elements = run_overpass_to_map(
query,
previous_elements,
previous_spatial,
layer_color,
base_gdf1,
base_gdf2,
)
# 4 output: üretilen query, harita, birikmiş spatial metin, birikmiş element listesi
return query, map_html, combined_spatial, all_elements
def summarize_overpass_data(data: dict, max_examples: int = 30) -> str:
"""
Overpass JSON sonucundan LLM'e verilecek metinsel bir özet üretir.
Çok büyük verilerde token patlamaması için sınırlı örnek verir.
"""
if not data:
return "Overpass sonucu boş veya geçersiz.\n"
elements = data.get("elements", [])
if not elements:
return "Overpass sonucu: hiç element bulunamadı.\n"
total_nodes = sum(1 for e in elements if e.get("type") == "node")
total_ways = sum(1 for e in elements if e.get("type") == "way")
total_rel = sum(1 for e in elements if e.get("type") == "relation")
# Basit tag istatistikleri
amenity_counts = {}
leisure_counts = {}
shop_counts = {}
highway_counts = {}
railway_counts = {}
pt_counts = {}
examples = []
for e in elements[:max_examples]:
etype = e.get("type")
tags = e.get("tags", {})
name = tags.get("name", "(isimsiz)")
amenity = tags.get("amenity")
leisure = tags.get("leisure")
shop = tags.get("shop")
highway = tags.get("highway")
railway = tags.get("railway")
pt = tags.get("public_transport")
opening_hours = tags.get("opening_hours")
wheelchair = tags.get("wheelchair")
if amenity:
amenity_counts[amenity] = amenity_counts.get(amenity, 0) + 1
if leisure:
leisure_counts[leisure] = leisure_counts.get(leisure, 0) + 1
if shop:
shop_counts[shop] = shop_counts.get(shop, 0) + 1
if highway:
highway_counts[highway] = highway_counts.get(highway, 0) + 1
if railway:
railway_counts[railway] = railway_counts.get(railway, 0) + 1
if pt:
pt_counts[pt] = pt_counts.get(pt, 0) + 1
# Örnek satır
tag_parts = []
for k in ["amenity", "leisure", "shop", "highway", "railway", "public_transport",
"opening_hours", "wheelchair"]:
v = tags.get(k)
if v:
tag_parts.append(f"{k}={v}")
tag_text = ", ".join(tag_parts) if tag_parts else "etiket yok"
examples.append(f"- {etype} | {name} | {tag_text}")
def dict_to_lines(title, d):
if not d:
return []
items = sorted(d.items(), key=lambda x: -x[1])
lines = [title]
for k, v in items:
lines.append(f" - {k}: {v}")
return lines
lines = [
f"Toplam node sayısı: {total_nodes}",
f"Toplam way sayısı: {total_ways}",
f"Toplam relation sayısı: {total_rel}",
"",
]
lines += dict_to_lines("Amenity türleri:", amenity_counts)
lines += dict_to_lines("Leisure türleri:", leisure_counts)
lines += dict_to_lines("Shop türleri:", shop_counts)
lines += dict_to_lines("Highway türleri:", highway_counts)
lines += dict_to_lines("Railway türleri:", railway_counts)
lines += dict_to_lines("Public transport türleri:", pt_counts)
if examples:
lines.append("")
lines.append(f"İlk {len(examples)} elementten bazı örnekler:")
lines.extend(examples)
return "\n".join(lines)
# ==========================================
# LLM SOHBET FONKSİYONU
# ==========================================
def respond(
message,
history,
model_name,
system_message,
max_tokens,
temperature,
top_p,
compare_context, # Mahalle karşılaştırma
spatial_context, # Overpass sonuçlar
):
# --------------- OSM SPATIAL QUERY MODU ---------------
# --- /osm ile başlayan mesajlar: sadece Overpass sorgusu üret ---
if is_osm_query(message):
user_text = message.lstrip()[4:].strip()
if not user_text:
user_text = "Generate an Overpass QL query for my request."
client = InferenceClient(model=model_name, token=HF_TOKEN if HF_TOKEN else None)
messages = [
{"role": "system", "content": OSM_QUERY_SYSTEM},
{"role": "user", "content": user_text},
]
query_text = ""
for chunk in client.chat_completion(
messages=messages,
max_tokens=400,
stream=True,
temperature=0.2,
top_p=0.9,
):
choices = chunk.choices
token_text = ""
if len(choices) and choices[0].delta.content:
token_text = choices[0].delta.content
query_text += token_text
# Kullanıcıya sadece sorguyu göster
yield f"Üretilen Overpass Sorgusu:\n```ql\n{query_text}\n```"
return
# --------------- NORMAL CHAT AKIŞI ---------------
"""
Streaming chat using Hugging Face Inference API.
history: list of {"role": "...", "content": "..."}
"""
# Temperature güvenli aralık (model max 2)
temperature = max(0.0, min(2.0, float(temperature)))
top_p = max(0.0, min(1.0, float(top_p)))
client = InferenceClient(model=model_name, token=HF_TOKEN if HF_TOKEN else None)
# System mesajına mahalle karşılaştırma bağlamını ekle
full_system = system_message
if compare_context:
full_system += (
"\n\nAşağıda aynı şehirdeki iki mahalleye ait sayısal özetler var.\n"
"Kullanıcı bu mahalleler hakkında soru sorarsa bu bağlama göre cevap ver:\n"
f"{compare_context}"
)
if spatial_context:
full_system += (
"\n\nAyrıca kullanıcı tarafından en son çalıştırılan bir Overpass (spatial) sorgusunun"
" özet sonuçları var. Kullanıcı bu sorgudan gelen veriler hakkında soru sorarsa,"
" bu özet bağlamına dayanarak cevap ver:\n"
f"{spatial_context}"
)
messages = [{"role": "system", "content": full_system}]
messages.extend(history)
messages.append({"role": "user", "content": message})
response = ""
for chunk in client.chat_completion(
messages=messages,
max_tokens=max_tokens,
stream=True,
temperature=temperature,
top_p=top_p,
):
choices = chunk.choices
token_text = ""
if len(choices) and choices[0].delta.content:
token_text = choices[0].delta.content
response += token_text
yield response + f"\n\n---\n**Model:** {model_name}"
# ==========================================
# GRADIO ARAYÜZÜ (SOL CHAT, SAĞ KARŞILAŞTIRMA PANELİ)
# ==========================================
with gr.Blocks() as demo:
gr.Markdown("## Mahalle Karşılaştırmalı Chat Botu")
compare_state = gr.State("")
spatial_state = gr.State("") # son Overpass özetleri
overpass_elements_state = gr.State([]) # 👈 tüm Overpass sonuçlarını biriktireceğimiz liste
gdf1_state = gr.State(None) # 1. mahalle geometri
gdf2_state = gr.State(None) # 2. mahalle geometri
with gr.Row():
# SOL SÜTUN: CHAT
with gr.Column(scale=2):
chatbox = gr.Chatbot(height=800, scale=1)
model_dropdown = gr.Dropdown(
choices=[
# Küçük modeller
"google/gemma-2-2b-it", # 2B
"meta-llama/Meta-Llama-3.1-8B-Instruct", # 8B
# Büyük modeller
"abacusai/Dracarys-72B-Instruct", # 72B
"Qwen/Qwen2.5-72B-Instruct", # 72B
# Çok büyük model
"openai/gpt-oss-120b", # 120Bdı
],
label="Model Seç (Bu listedeki modeller Hugging Face Inference API chat_completion ile uyumludur)",
value="google/gemma-2-2b-it"
)
system_box = gr.Textbox(
value="Sen şehir planlama ve mahalleler hakkında bilgi veren yardımsever bir asistansın.",
label="System message",
)
max_tokens_slider = gr.Slider(
minimum=1,
maximum=2048,
value=512,
step=1,
label="Max new tokens",
)
temperature_slider = gr.Slider(
minimum=0.0,
maximum=2.0, # model max 2
value=0.5,
step=0.1,
label="Temperature",
)
top_p_slider = gr.Slider(
minimum=0.1,
maximum=1.0,
value=0.5,
step=0.05,
label="Top-p (nucleus sampling)",
)
chatbot = gr.ChatInterface(
respond,
chatbot=chatbox,
type="messages",
title="Basit Chat Botu",
description="Küçük bir sohbet botu, HF Inference API ve OSM verisi ile çalışıyor.",
additional_inputs=[
model_dropdown,
system_box,
max_tokens_slider,
temperature_slider,
top_p_slider,
compare_state, # >>> mahalle karşılaştırma bağlamı
spatial_state, # >>> son Overpass sonucu özeti
],
)
# SAĞ SÜTUN: MAHALLE KARŞILAŞTIRMA PANELİ
with gr.Column(scale=1):
gr.Markdown("### Mahalle Karşılaştırma")
# 1. mahalle: ilçe + mahalle aynı satırda
with gr.Row():
city_in1 = gr.Textbox(
label="1. Şehir",
value="Ankara",
placeholder="Örn: Ankara",
)
district1_in = gr.Textbox(
label="1. İlçe",
value="Gölbaşı",
scale=1,
placeholder="Örn: Gölbaşı",
)
neigh1_in = gr.Textbox(
label="1. Mahalle",
value="İncek",
scale=2,
placeholder="Örn: İncek Mahallesi",
)
# 2. mahalle: ilçe + mahalle aynı satırda
with gr.Row():
city_in2 = gr.Textbox(
label="2. Şehir",
value="Ankara",
placeholder="Örn: Ankara",
)
district2_in = gr.Textbox(
label="2. İlçe",
value="Gölbaşı",
scale=1,
placeholder="Örn: Gölbaşı",
)
neigh2_in = gr.Textbox(
label="2. Mahalle",
value="Kızılcaşar",
scale=2,
placeholder="Örn: Kızılcaşar Mahallesi",
)
compare_btn = gr.Button("Karşılaştırmayı Hazırla")
with gr.Row():
stats1_box = gr.Textbox(
label="1. Mahalle Özeti",
lines=4,
)
stats2_box = gr.Textbox(
label="2. Mahalle Özeti",
lines=4,
)
map_html = gr.HTML(label="Mahalle Haritası")
gr.Markdown("### Spatial Query (Overpass)")
# 1) LLM'e doğal dil prompt'u
osm_nl_prompt = gr.Textbox(
label="LLM ile Overpass Sorgusu (Doğal Dil)",
lines=3,
placeholder="Örn: İncek ve Kızılcaşar çevresindeki tüm park ve okulları getir"
)
gen_and_run_btn = gr.Button("LLM ile Sorguyu Üret ve Çalıştır")
# 2) Üretilen veya manuel Overpass sorgusu
overpass_box = gr.Textbox(
label="Overpass Sorgusu",
lines=6,
placeholder="Buraya LLM'in ürettiği Overpass QL sorgusunu yapıştırın..."
)
layer_color_dd = gr.Dropdown(
choices=["red", "blue", "green", "orange", "purple"],
value="red",
label="Spatial katman rengi"
)
run_overpass_btn = gr.Button("Sorguyu Çalıştır ve Haritayı Güncelle")
# En yakın POI (generic)
poi_type_dropdown = gr.Dropdown(
choices=["Üniversite", "Market", "Hastane", "Eczane", "Park"],
value="Üniversite",
label="En yakın neyi bulmak istiyorsun?"
)
nearest_poi_btn = gr.Button("1. Mahalle için en yakın noktayı bul")
nearest_poi_box = gr.Textbox(
label="En Yakın POI",
lines=2,
)
nearest_poi_btn.click(
fn=nearest_poi_wrapper,
inputs=[city_in1, district1_in, neigh1_in, poi_type_dropdown],
outputs=[nearest_poi_box],
)
# LLM ile üret + çalıştır (birikimli)
gen_and_run_btn.click(
fn=llm_overpass_to_map,
inputs=[osm_nl_prompt, model_dropdown, overpass_elements_state,
spatial_state, layer_color_dd, gdf1_state, gdf2_state],
outputs=[overpass_box, map_html, spatial_state, overpass_elements_state],
)
# Manuel Overpass çalıştırma (birikimli)
run_overpass_btn.click(
fn=run_overpass_to_map,
inputs=[overpass_box, overpass_elements_state, spatial_state,
layer_color_dd, gdf1_state, gdf2_state],
outputs=[map_html, spatial_state, overpass_elements_state],
)
compare_btn.click(
fn=prepare_comparison,
inputs=[city_in1, district1_in, neigh1_in, city_in1, district2_in, neigh2_in],
outputs=[stats1_box, stats2_box, compare_state, map_html, gdf1_state, gdf2_state],
)
if __name__ == "__main__":
demo.launch()