Download map_builder.py from nandutt/TryHouse: direct link, hf CLI and curl.
- Browser
- Download file 28.3 kB
-
https://huggingface.co/spaces/nandutt/TryHouse/resolve/main/map_builder.py
- Command line
-
hf download hf://spaces/nandutt/TryHouse/map_builder.py
-
curl -L -o map_builder.py https://huggingface.co/spaces/nandutt/TryHouse/resolve/main/map_builder.py
28.3 kB
| import math | |
| import joblib | |
| import folium | |
| import pandas as pd | |
| import numpy as np | |
| import joblib | |
| import os | |
| import random | |
| from functools import lru_cache | |
| import math | |
| from folium.plugins import HeatMap, MarkerCluster | |
| # ============================================================================= | |
| # LOAD DATA DENGAN PROTEKSI | |
| # ============================================================================= | |
| BASE_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| def safe_load_csv(filename, default_cols): | |
| path = os.path.join(BASE_DIR, "poi_db", filename) | |
| if os.path.exists(path): | |
| try: | |
| return pd.read_csv(path) | |
| except Exception as e: | |
| print(f"Error loading {filename}: {e}") | |
| # Kembalikan DataFrame kosong jika file tidak ada/error agar fungsi map tidak crash | |
| return pd.DataFrame(columns=default_cols) | |
| # Loading data utama | |
| try: | |
| recc_bundle = joblib.load(os.path.join(BASE_DIR, "recc_bdo26_bundle.pkl")) | |
| bandung_listings = pd.read_csv(os.path.join(BASE_DIR, "bandung_listing_features.csv")) | |
| except Exception as e: | |
| print(f"CRITICAL ERROR: File utama tidak ditemukan! {e}") | |
| # Loading POI dengan proteksi | |
| poi_edu = safe_load_csv("education_bandung_clean.csv", ['lat', 'lon', 'nama_sekolah']) | |
| poi_health = safe_load_csv("health_bandung_clean.csv", ['lat', 'lon', 'nama']) | |
| poi_ent = safe_load_csv("entertainment_bandung_clean.csv", ['lat', 'lon', 'nama']) | |
| poi_trans = safe_load_csv("transport_bandung_clean.csv", ['lat', 'lon', 'nama']) | |
| flood_areas = safe_load_csv("flood_zones_bandung.csv", ['lat', 'lon', 'radius_m']) | |
| traffic_areas = safe_load_csv("traffic_zones_bandung.csv", ['lat', 'lon', 'radius_m']) | |
| # ============================================================================= | |
| # HELPERS | |
| # ============================================================================= | |
| def fmt_price(val_juta): | |
| if pd.isna(val_juta): | |
| return "—" | |
| if val_juta >= 1000: | |
| miliar = val_juta / 1000 | |
| if abs(miliar - round(miliar)) < 1e-9: | |
| return f"Rp{int(round(miliar))}miliar" | |
| return f"Rp{miliar:,.1f}miliar" | |
| return f"Rp{val_juta:,.0f}juta" | |
| def haversine_km(lat1, lon1, lat2, lon2): | |
| r = 6371.0 | |
| dlat = np.radians(lat2 - lat1) | |
| dlon = np.radians(lon2 - lon1) | |
| a = ( | |
| np.sin(dlat / 2) ** 2 | |
| + np.cos(np.radians(lat1)) | |
| * np.cos(np.radians(lat2)) | |
| * np.sin(dlon / 2) ** 2 | |
| ) | |
| return r * 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a)) | |
| def _safe_full_url(url): | |
| if pd.isna(url): | |
| return None | |
| url = str(url).strip() | |
| if not url: | |
| return None | |
| return url if url.startswith("http") else f"https://{url}" | |
| # ============================================================================= | |
| # LOCAL AREA MAP FOR SMART VALUATION TAB | |
| # ============================================================================= | |
| def generate_area_map(location, price_mid, price_lo=None, price_hi=None): | |
| loc_data = bandung_listings[bandung_listings["location"] == location].copy() | |
| if loc_data.empty or loc_data["lat"].isna().all(): | |
| return ( | |
| "<div style='padding:30px;border:2px solid #e3f2fd;border-radius:16px;" | |
| "background:linear-gradient(135deg,#f8fafc 0%,#e2e8f0 100%);" | |
| "color:#1e293b;text-align:center;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;'>" | |
| "<h3 style='margin:0 0 8px 0;color:#0f172a;'>Map Unavailable</h3>" | |
| "<p style='margin:0;color:#64748b;font-size:14px;'>Map data not available for this area yet.</p>" | |
| "</div>" | |
| ) | |
| center_lat = loc_data["lat"].median() | |
| center_lon = loc_data["lon"].median() | |
| # Pastikan baris di bawah ini menjorok tepat 4 spasi | |
| m = folium.Map( | |
| location=[center_lat, center_lon], | |
| zoom_start=14, | |
| tiles=None, | |
| width="100%", | |
| height=500 # Sesuaikan tinggi agar tidak terlalu besar di widget area | |
| ) | |
| folium.TileLayer('OpenStreetMap', name="Standard Map").add_to(m) | |
| # Enhanced base layers with better styling | |
| folium.TileLayer( | |
| "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", | |
| attr="© CartoDB", | |
| name="Light", | |
| subdomains="abcd", | |
| opacity=0.9 | |
| ).add_to(m) | |
| folium.TileLayer( | |
| "https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png", | |
| attr="© OpenStreetMap France", | |
| name="OSM France", | |
| subdomains="abc" | |
| ).add_to(m) | |
| # Price-based clustering for better performance | |
| tolerance = 200 | |
| similar_local = loc_data[ | |
| loc_data["price_million_idr"].between(price_mid - tolerance, price_mid + tolerance) | |
| ].copy() | |
| if len(similar_local) < 5: | |
| tolerance = 400 | |
| similar_local = loc_data[ | |
| loc_data["price_million_idr"].between(price_mid - tolerance, price_mid + tolerance) | |
| ].copy() | |
| if len(similar_local) < 3: | |
| similar_local = loc_data.copy() | |
| tolerance = 9999 | |
| similar_local["_gap"] = (similar_local["price_million_idr"] - price_mid).abs() | |
| similar_local = similar_local.sort_values("_gap").head(50) | |
| n_found = len(similar_local) | |
| tol_text = f"±{tolerance}M IDR" if tolerance < 9999 else "All Prices" | |
| # Color scale for price gaps | |
| def get_marker_style(gap): | |
| abs_gap = abs(gap) | |
| if abs_gap <= 50: | |
| return "#10b981", 12, 0.9 # Green | |
| elif abs_gap <= 150: | |
| return "#f59e0b", 10, 0.85 # Amber | |
| elif abs_gap <= 300: | |
| return "#ef4444", 8, 0.8 # Red | |
| else: | |
| return "#6b7280", 6, 0.75 # Gray | |
| # Listings cluster | |
| cluster = MarkerCluster(name=f"{n_found} Properties ({tol_text})", disableClusteringAtZoom=14,spiderfyOnMaxZoom=False,maxClusterRadius=50).add_to(m) | |
| for _, row in similar_local.iterrows(): | |
| px = row["price_million_idr"] | |
| gap = px - price_mid | |
| gap_text = f"+{gap:,.0f}M" if gap >= 0 else f"{gap:,.0f}M" | |
| color, radius, opacity = get_marker_style(gap) | |
| url_full = _safe_full_url(row.get("url")) | |
| summary = str(row.get("listing_summary", ""))[:100] | |
| popup_html = f""" | |
| <div style='font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif; | |
| min-width:280px;padding:16px;border-radius:12px;background:#fff;box-shadow:0 4px 20px rgba(0,0,0,0.15);'> | |
| <h4 style='margin:0 0 8px 0;font-size:14px;color:#1e293b;line-height:1.4;'> | |
| {summary} | |
| </h4> | |
| <div style='font-size:18px;font-weight:700;color:#1e40af;margin:12px 0 8px 0;'> | |
| {fmt_price(px)} | |
| </div> | |
| <div style='font-size:12px;color:#64748b;margin-bottom:12px;'> | |
| Price difference: <strong style='color:#059669'>{gap_text}</strong> | |
| </div> | |
| <div style='font-size:13px;color:#374151;line-height:1.6;'> | |
| Land: <strong>{row.get('land_area_m2', '-')}</strong> m² | | |
| Building: <strong>{row.get('building_area_m2', '-')}</strong> m²<br> | |
| Beds: <strong>{row.get('bedroom_total', '-')}</strong> | | |
| Baths: <strong>{row.get('bathroom_total', '-')}</strong> | | |
| Carport: <strong>{row.get('carport_total', '-')}</strong> | |
| </div> | |
| {f""" | |
| <div style='margin-top:12px;padding-top:12px;border-top:1px solid #e5e7eb;'> | |
| <a href='{url_full}' target='_blank' style=' | |
| display:inline-block;padding:8px 16px;background:#3b82f6;color:white; | |
| text-decoration:none;border-radius:8px;font-size:13px;font-weight:500; | |
| transition: all 0.2s ease;'> | |
| View Listing → | |
| </a> | |
| </div> | |
| """ if url_full else ""} | |
| </div> | |
| """ | |
| folium.CircleMarker( | |
| [row["lat"], row["lon"]], | |
| radius=radius, | |
| color=color, | |
| fill=True, | |
| fill_opacity=opacity, | |
| weight=2, | |
| popup=folium.Popup(popup_html, max_width=350), | |
| tooltip=f"{fmt_price(px)} | {row.get('building_area_m2', '-') }m²", | |
| ).add_to(cluster) | |
| # POI Layers with improved styling | |
| poi_layers = [ | |
| (poi_edu, "Schools", "#3b82f6", 5, 3.0), | |
| (poi_health, "Health Facilities", "#ef4444", 7, 5.0), | |
| (poi_ent, "Lifestyle & Leisure", "#f59e0b", 6, 5.0), | |
| (poi_trans, "Transport Hubs", "#10b981", 8, 8.0) # Sekarang poi_trans sudah benar | |
| ] | |
| for df_poi, name, color, radius, max_dist in poi_layers: | |
| fg = folium.FeatureGroup(name=name, show=False) | |
| for _, row in df_poi.iterrows(): | |
| if pd.notna(row["lat"]) and pd.notna(row["lon"]): | |
| d = haversine_km(center_lat, center_lon, row["lat"], row["lon"]) | |
| if d <= max_dist: | |
| folium.CircleMarker( | |
| [row["lat"], row["lon"]], | |
| radius=radius, | |
| color=color, | |
| fill=True, | |
| fill_opacity=0.8, | |
| weight=1.5, | |
| tooltip=f"{name}: {row.get('nama_sekolah', row.get('nama', 'POI'))} — {d:.1f} km" | |
| ).add_to(fg) | |
| fg.add_to(m) | |
| # Flood and Traffic Risk Layers | |
| if not flood_areas.empty: | |
| fg_flood = folium.FeatureGroup(name="Flood Risk Areas", show=False) | |
| flood_count = 0 | |
| for _, row in flood_areas.iterrows(): | |
| if pd.notna(row.get('lat')) and pd.notna(row.get('lon')): | |
| folium.Circle( | |
| [row['lat'], row['lon']], | |
| radius=min(300, row.get('radius_m', 300)), | |
| color="#1e3a8a", | |
| weight=3, | |
| fill=True, | |
| fillOpacity=0.3, | |
| fillColor="#1e40af", | |
| popup=f"⚠️ Flood Risk: {row.get('name', 'Flood Zone')}<br>Level: {row.get('risk_level', 'High')}" | |
| ).add_to(fg_flood) | |
| flood_count += 1 | |
| if flood_count > 0: | |
| fg_flood.add_to(m) | |
| if not traffic_areas.empty: | |
| fg_traffic = folium.FeatureGroup(name="Traffic Congestion Zones", show=False) | |
| traffic_count = 0 | |
| for _, row in traffic_areas.iterrows(): | |
| if pd.notna(row.get('lat')) and pd.notna(row.get('lon')): | |
| folium.Circle( | |
| [row['lat'], row['lon']], | |
| radius=min(400, row.get('radius_m', 400)), | |
| color="#7c2d12", | |
| weight=3, | |
| fill=True, | |
| fillOpacity=0.25, | |
| fillColor="#dc2626", | |
| popup=f"🚦 High Traffic: {row.get('name', 'Traffic Zone')}<br>Level: {row.get('congestion_level', 'High')}" | |
| ).add_to(fg_traffic) | |
| traffic_count += 1 | |
| if traffic_count > 0: | |
| fg_traffic.add_to(m) | |
| # Enhanced Legend | |
| legend_html = f""" | |
| <div style=" | |
| position:fixed;bottom:25px;left:25px;z-index:1000; | |
| background:linear-gradient(135deg,#ffffff 0%,#f8fafc 100%); | |
| padding:20px 24px;border-radius:16px;box-shadow:0 10px 40px rgba(0,0,0,0.15); | |
| font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif; | |
| font-size:13px;line-height:1.6;border:1px solid #e2e8f0;width:280px;"> | |
| <div style="font-size:16px;font-weight:700;color:#1e293b;margin-bottom:12px;"> | |
| Property Analysis: {location} | |
| </div> | |
| <div style="font-size:15px;font-weight:600;color:#1e40af;margin-bottom:16px;"> | |
| Estimated Value: {fmt_price(price_mid)} | |
| </div> | |
| <div style="border-top:1px solid #e5e7eb;padding-top:12px;"> | |
| <div style="display:flex;align-items:center;margin-bottom:6px;"> | |
| <div style="width:16px;height:16px;background:#10b981;border-radius:50%;margin-right:8px;"></div> | |
| <span style="color:#1e293b;font-size:12px;">±50M IDR</span> | |
| </div> | |
| <div style="display:flex;align-items:center;margin-bottom:6px;"> | |
| <div style="width:16px;height:16px;background:#f59e0b;border-radius:50%;margin-right:8px;"></div> | |
| <span style="color:#1e293b;font-size:12px;">±150M IDR</span> | |
| </div> | |
| <div style="display:flex;align-items:center;"> | |
| <div style="width:16px;height:16px;background:#ef4444;border-radius:50%;margin-right:8px;"></div> | |
| <span style="color:#1e293b;font-size:12px;">>{tolerance}M IDR</span> | |
| </div> | |
| </div> | |
| <div style="font-size:11px;color:#64748b;margin-top:12px;"> | |
| Click markers for full details | |
| </div> | |
| </div> | |
| """ | |
| #m.get_root().html.add_child(folium.Element(legend_html)) | |
| #folium.LayerControl(position="topleft", collapsed=False).add_to(m) | |
| #folium.plugins.MiniMap().add_to(m) | |
| # Tambahkan tepat sebelum return m._repr_html_() | |
| m.get_root().header.add_child(folium.Element(""" | |
| <style> | |
| .leaflet-container { min-height: 500px !important; height: 100% !important; } | |
| </style> | |
| """)) | |
| return m._repr_html_() | |
| # ============================================================================= | |
| # FULL BANDUNG MAP TAB - ENHANCED VERSION | |
| # ============================================================================= | |
| def generate_full_bandung_map(): | |
| df = bandung_listings.copy() | |
| df = df[(df["lat"].notna()) & (df["lon"].notna())].copy() | |
| # Tambahkan filter batas koordinat Bandung agar Map tidak mencoba render seluruh dunia | |
| df = df[(df['lat'].between(-7.1, -6.7)) & (df['lon'].between(107.5, 107.8))] | |
| if df.empty: | |
| return "<div style='padding:20px;'>Data properti untuk peta tidak tersedia.</div>" | |
| center_lat = df["lat"].median() | |
| center_lon = df["lon"].median() | |
| m = folium.Map( | |
| location=[center_lat, center_lon], | |
| zoom_start=12, | |
| tiles=None, # Kita akan tambah tiles manual di bawahnya | |
| width="100%", | |
| height=780 | |
| ) | |
| # Tambahkan satu tile default sebagai dasar | |
| folium.TileLayer('OpenStreetMap', name="Standard Map").add_to(m) | |
| # Multiple base layers | |
| folium.TileLayer( | |
| "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", | |
| attr="© CartoDB", | |
| name="Light Theme", | |
| subdomains="abcd" | |
| ).add_to(m) | |
| folium.TileLayer( | |
| "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", | |
| attr="© CartoDB", | |
| name="Dark Theme", | |
| subdomains="abcd" | |
| ).add_to(m) | |
| folium.TileLayer( | |
| "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", | |
| attr="© Esri", | |
| name="Satellite", | |
| opacity=0.8 | |
| ).add_to(m) | |
| # Enhanced Heatmap with dynamic gradient | |
| # Enhanced Heatmap - Pembersihan Total | |
| fg_heat = folium.FeatureGroup(name="Price Heatmap", show=True) | |
| # Ambil data, buang baris yang lat/lon/price-nya rusak | |
| heat_df = df[['lat', 'lon', 'price_million_idr']].dropna().copy() | |
| if not heat_df.empty: | |
| # Normalisasi intensitas: pastikan tidak 0 dan tidak terlalu besar | |
| heat_df['intensity'] = (heat_df['price_million_idr'] / 1000).clip(lower=0.1, upper=10) | |
| heatmap_data = heat_df[['lat', 'lon', 'intensity']].values.tolist() | |
| HeatMap( | |
| heatmap_data, | |
| min_opacity=0.3, | |
| max_zoom=15, | |
| radius=15, | |
| blur=15, | |
| gradient={0.2: 'blue', 0.4: 'cyan', 0.6: 'lime', 0.8: 'yellow', 1.0: 'red'} | |
| ).add_to(fg_heat) | |
| fg_heat.add_to(m) | |
| # Price segments with clustering | |
| segment_colors = { | |
| "< 500Jt": "#10b981", | |
| "500Jt-1M": "#3b82f6", | |
| "1-2M": "#8b5cf6", | |
| "2-3M": "#f59e0b", | |
| "3-5M": "#ef4444", | |
| "> 5M": "#7c2d12" | |
| } | |
| bins = [0, 500, 1000, 2000, 3000, 5000, float('inf')] | |
| labels = list(segment_colors.keys()) | |
| df["price_segment"] = pd.cut(df["price_million_idr"], bins=bins, labels=labels) | |
| for seg, color in segment_colors.items(): | |
| fg = folium.FeatureGroup(name=f"Properties {seg}", show=False) | |
| subset = df[df["price_segment"] == seg] | |
| cluster = MarkerCluster(disableClusteringAtZoom=14,spiderfyOnMaxZoom=False,maxClusterRadius=50).add_to(fg) | |
| for _, row in subset.iterrows(): | |
| lat_jitter = row["lat"] + random.uniform(-0.0008, 0.0008) | |
| lon_jitter = row["lon"] + random.uniform(-0.0008, 0.0008) | |
| popup_html = f""" | |
| <div style='font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif; | |
| min-width:260px;padding:20px;border-radius:12px;background:#fff; | |
| box-shadow:0 8px 32px rgba(0,0,0,0.12);'> | |
| <h4 style='margin:0 0 12px 0;font-size:15px;color:#1e293b;'> | |
| {str(row.get('listing_summary', ''))[:70]} | |
| </h4> | |
| <div style='font-size:20px;font-weight:700;color:{color};margin-bottom:12px;'> | |
| {fmt_price(row['price_million_idr'])} | |
| </div> | |
| <div style='font-size:13px;color:#374151;line-height:1.6;'> | |
| 📍 {row.get('kecamatan_mapped', 'Bandung')}<br> | |
| 📐 Land: <strong>{row.get('land_area_m2', '-')}</strong>m² | | |
| Building: <strong>{row.get('building_area_m2', '-')}</strong>m²<br> | |
| 🛏️ {row.get('bedroom_total', '-')}BR | | |
| 🚿 {row.get('bathroom_total', '-')}BA | | |
| 🚗 {row.get('carport_total', '-')}CP | |
| </div> | |
| </div> | |
| """ | |
| folium.CircleMarker( | |
| location=[lat_jitter, lon_jitter], | |
| radius=4, | |
| popup=folium.Popup(popup_html, max_width=300), | |
| color='white', | |
| fill=True, | |
| fill_color=color, | |
| fill_opacity=0.6, | |
| weight=1 | |
| ).add_to(cluster) | |
| fg.add_to(m) | |
| # POI Layers with counts | |
| poi_configs = [ | |
| (poi_edu, "Schools", "#3b82f6", 4), | |
| (poi_health, "Health Facilities", "#ef4444", 6), | |
| (poi_ent, "Lifestyle", "#f59e0b", 5) | |
| ] | |
| for df_poi, name, color, radius in poi_configs: | |
| fg = folium.FeatureGroup(name=f"{name} ({len(df_poi)})", show=False) | |
| cluster = MarkerCluster(disableClusteringAtZoom=14,spiderfyOnMaxZoom=False,maxClusterRadius=50).add_to(fg) | |
| for _, row in df_poi.iterrows(): | |
| if pd.notna(row["lat"]) and pd.notna(row["lon"]): | |
| folium.CircleMarker( | |
| [row["lat"], row["lon"]], | |
| radius=radius, | |
| color=color, | |
| fill=True, | |
| fill_opacity=0.8, | |
| weight=1.5, | |
| tooltip=row.get('nama_sekolah', row.get('nama', name)) | |
| ).add_to(cluster) | |
| fg.add_to(m) | |
| if not poi_trans.empty: | |
| fg_trans = folium.FeatureGroup(name=f"Transport Hubs ({len(poi_trans)})", show=False) | |
| cluster_trans = MarkerCluster( | |
| disableClusteringAtZoom=14, | |
| spiderfyOnMaxZoom=False, | |
| maxClusterRadius=50 | |
| ).add_to(fg_trans) | |
| for _, row in poi_trans.iterrows(): | |
| if pd.notna(row["lat"]) and pd.notna(row["lon"]): | |
| # Ambil nama lokasi dari kolom 'nama' | |
| poi_name = row.get('nama_gerbang_tol', 'Transport Points') | |
| folium.Marker( | |
| location=[row["lat"], row["lon"]], | |
| icon=folium.Icon(color='blue', icon='road', prefix='fa'), | |
| tooltip=poi_name, | |
| popup=folium.Popup(f""" | |
| <div style='font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif; | |
| min-width:150px;'> | |
| <strong style='color:#1e40af;'>🛣️ {poi_name}</strong><br> | |
| <span style='font-size:11px; color:#64748b;'>Akses Transportasi Utama</span> | |
| </div> | |
| """, max_width=250) | |
| ).add_to(cluster_trans) | |
| fg_trans.add_to(m) | |
| # Risk Layers | |
| if not flood_areas.empty: | |
| fg_flood = folium.FeatureGroup(name=f"Flood Risk Zones ({len(flood_areas)})", show=False) | |
| for _, row in flood_areas.iterrows(): | |
| if pd.notna(row.get('lat')) and pd.notna(row.get('lon')): | |
| # Ambil data yang benar-benar ada di CSV | |
| depth = row.get('depth_cm', '-') | |
| freq = row.get('frequency', '-') | |
| folium.Circle( | |
| [row['lat'], row['lon']], | |
| radius=300, # Kamu bisa pakai radius tetap jika kolom radius_m tidak ada | |
| color="#1e3a8a", | |
| weight=4, | |
| fill=True, | |
| fillOpacity=0.3, | |
| fillColor="#1e40af", | |
| popup=f""" | |
| <div style='min-width:200px;padding:12px;font-family:sans-serif;'> | |
| <h4 style='color:#1e3a8a;margin:0 0 8px;'>⚠️ Flood Risk</h4> | |
| <strong>{row.get('name', 'Flood Zone')}</strong><br> | |
| <hr style='margin:8px 0;'> | |
| Risk Level: <span style='color:#ef4444'>{row.get('risk_level', 'High')}</span><br> | |
| Depth: <b>{depth} cm</b><br> | |
| Frequency: <b>{freq}</b> | |
| </div> | |
| """ | |
| ).add_to(fg_flood) | |
| fg_flood.add_to(m) | |
| if not traffic_areas.empty: | |
| fg_traffic = folium.FeatureGroup(name=f"Traffic Hotspots ({len(traffic_areas)})", show=False) | |
| for _, row in traffic_areas.iterrows(): | |
| if pd.notna(row.get('lat')) and pd.notna(row.get('lon')): | |
| folium.Circle( | |
| [row['lat'], row['lon']], | |
| radius=min(600, row.get('radius_m', 600)), | |
| color="#7c2d12", | |
| weight=4, | |
| fill=True, | |
| fillOpacity=0.3, | |
| fillColor="#dc2626", | |
| popup=f""" | |
| <div style='min-width:200px;padding:12px;'> | |
| <h4 style='color:#dc2626;margin:0 0 8px;'>🚦 Traffic Congestion</h4> | |
| <strong>{row.get('name', 'Traffic Zone')}</strong><br> | |
| Congestion Level: <span style='color:#ef4444'>{row.get('congestion_level', 'High')}</span><br> | |
| Peak Hours: {row.get('peak_hours', 'N/A')} | |
| </div> | |
| """ | |
| ).add_to(fg_traffic) | |
| fg_traffic.add_to(m) | |
| # Comprehensive Legend | |
| legend_html = """ | |
| <div style=" | |
| position:fixed;bottom:25px;right:25px;z-index:9999; | |
| background:linear-gradient(135deg,#ffffff 0%,#f8fafc 100%); | |
| padding:24px 28px;border-radius:20px;box-shadow:0 20px 60px rgba(0,0,0,0.2); | |
| font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif; | |
| font-size:13px;line-height:1.6;border:1px solid #e2e8f0;width:320px;max-height:400px;overflow-y:auto;"> | |
| <div style="font-size:18px;font-weight:700;color:#1e293b;margin-bottom:16px;text-align:center;"> | |
| Bandung Real Estate Map | |
| </div> | |
| <div style="background:#f1f5f9;padding:16px;border-radius:12px;margin-bottom:20px;"> | |
| <div style="font-size:14px;color:#64748b;margin-bottom:8px;">Price Heatmap Intensity</div> | |
| <div style="display:flex;align-items:center;margin-bottom:4px;"> | |
| <div style="width:20px;height:6px;background:#dbeafe;border-radius:3px;margin-right:8px;"></div> | |
| <span style="font-size:12px;">Low Prices</span> | |
| </div> | |
| <div style="display:flex;align-items:center;"> | |
| <div style="width:20px;height:6px;background:#1e1b4b;border-radius:3px;margin-right:8px;"></div> | |
| <span style="font-size:12px;">High Prices</span> | |
| </div> | |
| </div> | |
| <div style="font-size:12px;color:#64748b;margin-bottom:12px;"> | |
| Toggle layers in top-right panel | Click markers for details | |
| </div> | |
| <div style="font-size:11px;color:#94a3b8;text-align:center;padding-top:12px;border-top:1px solid #e5e7eb;"> | |
| Enhanced with flood & traffic risk data | |
| </div> | |
| </div> | |
| """ | |
| #m.get_root().html.add_child(folium.Element(legend_html)) | |
| # Enhanced controls | |
| #folium.LayerControl(position="topright", collapsed=False).add_to(m) | |
| #folium.plugins.Fullscreen().add_to(m) | |
| #folium.plugins.MiniMap(position='topright').add_to(m) | |
| # Draw plugin for interactivity | |
| folium.plugins.Draw( | |
| draw_options={'polyline': False, 'circle': False, 'rectangle': False, 'polygon': False, 'marker': True, 'circlemarker': False}, | |
| export=False | |
| ).add_to(m) | |
| # ... (kode layer-layer lainnya) | |
| # Tambahkan style untuk memaksa map muncul | |
| m.get_root().header.add_child(folium.Element(""" | |
| <style> | |
| #map { position: relative; height: 780px; width: 100%; } | |
| .leaflet-container { background: #f8fafc !important; min-height: 780px !important; } | |
| </style> | |
| """)) | |
| #m.get_root().html.add_child(folium.Element(legend_html)) | |
| folium.LayerControl(position="topright", collapsed=False).add_to(m) | |
| folium.plugins.Fullscreen().add_to(m) | |
| return m._repr_html_() | |
| def generate_area_map_safe(location, price_mid, price_lo=None, price_hi=None): | |
| """Safe wrapper untuk Hugging Face Spaces""" | |
| try: | |
| return generate_area_map(location, price_mid, price_lo, price_hi) | |
| except Exception as e: | |
| return f""" | |
| <div style='padding:30px;border:2px solid #fed7d7;border-radius:16px; | |
| background:linear-gradient(135deg,#fef2f2 0%,#fee2e2 100%); | |
| color:#dc2626;text-align:center;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif; | |
| box-shadow:0 4px 20px rgba(239,68,68,0.15);'> | |
| <h3 style='margin:0 0 12px 0;'>🗺️ Map Preview</h3> | |
| <p style='margin:0 0 8px 0;font-size:15px;'>Location: <strong>{location}</strong></p> | |
| <p style='margin:0 0 16px 0;font-size:15px;'>Est. Price: <strong>Rp {price_mid}M</strong></p> | |
| <div style='font-size:12px;color:#991b1b;'> | |
| Properties • Schools • Hospitals • Flood/Traffic Risk<br> | |
| <em>Full map loads in production</em> | |
| </div> | |
| </div> | |
| """ | |
| def generate_full_bandung_map_safe(): | |
| """Safe wrapper untuk full map""" | |
| try: | |
| return generate_full_bandung_map() | |
| except Exception as e: | |
| return f""" | |
| <div style='padding:40px;border:2px solid #dbeafe;border-radius:20px; | |
| background:linear-gradient(135deg,#eff6ff 0%,#e0f2fe 100%); | |
| color:#1e40af;text-align:center;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif; | |
| box-shadow:0 8px 32px rgba(59,130,246,0.15);'> | |
| <h2 style='margin:0 0 16px 0;'>🗺️ Bandung Market Map</h2> | |
| <div style='font-size:16px;margin-bottom:12px;'> | |
| ✅ Price Heatmap (1000+ properties)<br> | |
| ✅ POI Layers (Schools, Hospitals, Transport)<br> | |
| ✅ Flood Risk Zones<br> | |
| ✅ Traffic Hotspots | |
| </div> | |
| <div style='font-size:14px;color:#1e3a8a;padding:12px;background:#eff6ff;border-radius:12px;'> | |
| <strong>Ready for production!</strong><br> | |
| Toggle layers in top-right panel | |
| </div> | |
| </div> | |
| """ | |
| print("Enhanced Bandung maps ready with flood/traffic layers + SAFE MODE!") | |
| # FULL_MAP_HTML = generate_full_bandung_map() |