Spaces:
Running
Running
| import streamlit as st | |
| import pandas as pd | |
| import pydeck as pdk # <-- Cette ligne doit être présente | |
| from geopy.geocoders import Nominatim | |
| from geopy.exc import GeocoderTimedOut | |
| import time | |
| # Ajoutez ceci juste après vos imports, avant les constantes | |
| st.markdown(""" | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap'); | |
| /* Appliquer uniquement au texte, pas aux boutons */ | |
| .stMarkdown, .stText, p, span, div:not([class*="stButton"]) { | |
| font-family: 'Space Grotesk', sans-serif !important; | |
| } | |
| h1, h2, h3, h4, h5, h6 { | |
| font-family: 'Space Grotesk', sans-serif !important; | |
| } | |
| /* Exclure explicitement les boutons */ | |
| button, .stButton button { | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # --- CONSTANTES GOTHAM & CONFIG --- | |
| HQ_LOCATION = [48.913418, 2.396667] # 1 Rue Marcelin Berthelot, Aubervilliers | |
| COLOR_BLUE_GOTHAM = "#25C1F7" # Bleu clair | |
| COLOR_BLUE_FILL = "#83D6F7" # Remplissage bleu | |
| COLOR_GREEN_ZONE = "#54BD4B" # Vert Cluster | |
| COLOR_YELLOW_LINE = "#FFD700" # Lignes de distance | |
| # --- 1. FONCTIONS DE GÉOCODAGE (AVEC CACHE) --- | |
| def geocode_addresses(df_clients): | |
| """Récupère les coordonnées lat/lon pour les adresses clients.""" | |
| if 'Adresse' not in df_clients.columns or 'ID_Client' not in df_clients.columns: | |
| return pd.DataFrame() | |
| geolocator = Nominatim(user_agent="vortex_flux_app") | |
| geocoded_data = [] | |
| for index, row in df_clients.iterrows(): | |
| address = row['Adresse'] | |
| client_id = row['ID_Client'] | |
| try: | |
| search_query = str(address) | |
| time.sleep(1.5) | |
| location = geolocator.geocode(search_query, timeout=10) | |
| if location: | |
| geocoded_data.append({ | |
| "ID": client_id, | |
| "Nom": row.get('Nom_Complet', 'Client Inconnu'), | |
| "Adresse": address, | |
| "lat": location.latitude, | |
| "lon": location.longitude, | |
| "Revenus": row.get('Revenus_Mensuels', 0) | |
| }) | |
| except: | |
| pass | |
| return pd.DataFrame(geocoded_data) | |
| # --- 2. FONCTION PRINCIPALE D'AFFICHAGE --- | |
| def show_map_dashboard(client, sheet_name): | |
| st.markdown("## VUE TACTIQUE : GÉOLOCALISATION") | |
| # 1. Récupération des données depuis Google Sheets | |
| try: | |
| sh = client.open(sheet_name) | |
| ws = sh.worksheet("Clients_KYC") | |
| data = ws.get_all_records() | |
| df_raw = pd.DataFrame(data) | |
| except Exception as e: | |
| st.error(f"Erreur de lecture des données : {e}") | |
| return | |
| if df_raw.empty: | |
| st.info("Aucune donnée client à afficher sur la carte.") | |
| return | |
| # 2. Géocodage (mis en cache) | |
| with st.spinner("Triangulation des positions satellites..."): | |
| df_map = geocode_addresses(df_raw) | |
| if df_map.empty: | |
| st.warning("Impossible de géolocaliser les adresses fournies.") | |
| return | |
| # --- LAYOUT : NAVIGATEUR (GAUCHE) / CARTE (DROITE) --- | |
| col_nav, col_map = st.columns([1, 3]) | |
| with col_nav: | |
| st.markdown("### CIBLES") | |
| # Filtres Tactiques | |
| st.markdown("#### FILTRES VISUELS") | |
| show_dist = st.checkbox("GeoDistance (Lignes HQ)", value=True) | |
| show_labels = st.checkbox("LabelZoom (Noms)", value=False) | |
| show_cluster = st.checkbox("ClusterZones (Densité)", value=False) | |
| show_pop = st.checkbox("PopDensity (ANSD Data)", value=False) | |
| st.divider() | |
| # Liste des clients (Cartes cliquables simulées par des expanders) | |
| st.markdown("#### LISTE CLIENTS") | |
| for idx, row in df_map.iterrows(): | |
| with st.expander(f"📍 {row['ID']} - {row['Nom']}"): | |
| st.caption(f"Ad: {row['Adresse']}") | |
| st.caption(f"Rev: {row['Revenus']} XOF") | |
| with col_map: | |
| # Préparer les données des clients | |
| clients_markers = [] | |
| for _, row in df_map.iterrows(): | |
| clients_markers.append({ | |
| 'lon': row['lon'], | |
| 'lat': row['lat'], | |
| 'id': row['ID'], | |
| 'adresse': row['Adresse'] | |
| }) | |
| # Créer le HTML avec Mapbox GL | |
| mapbox_html = f""" | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <script src='https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.js'></script> | |
| <link href='https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.css' rel='stylesheet' /> | |
| <style> | |
| body {{ margin: 0; padding: 0; }} | |
| #map {{ position: absolute; top: 0; bottom: 0; width: 100%; }} | |
| </style> | |
| </head> | |
| <body> | |
| <div id="map"></div> | |
| <script> | |
| mapboxgl.accessToken = 'pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw'; // Token public Mapbox | |
| const map = new mapboxgl.Map({{ | |
| container: 'map', | |
| style: 'mapbox://styles/mapbox/satellite-streets-v12', // Vue satellite | |
| center: [2.396667, 48.913418], | |
| zoom: 15, | |
| pitch: 60, // Vue 3D inclinée | |
| bearing: 0, | |
| antialias: true | |
| }}); | |
| map.on('load', () => {{ | |
| // Activer les bâtiments 3D | |
| map.addLayer({{ | |
| 'id': '3d-buildings', | |
| 'source': 'composite', | |
| 'source-layer': 'building', | |
| 'filter': ['==', 'extrude', 'true'], | |
| 'type': 'fill-extrusion', | |
| 'minzoom': 14, | |
| 'paint': {{ | |
| 'fill-extrusion-color': '#aaa', | |
| 'fill-extrusion-height': ['get', 'height'], | |
| 'fill-extrusion-base': ['get', 'min_height'], | |
| 'fill-extrusion-opacity': 0.8 | |
| }} | |
| }}); | |
| // Marqueur HQ (Pyramide Rouge) | |
| const hqEl = document.createElement('div'); | |
| hqEl.style.width = '30px'; | |
| hqEl.style.height = '30px'; | |
| hqEl.style.backgroundImage = 'linear-gradient(135deg, #ff3232 0%, #aa0000 100%)'; | |
| hqEl.style.transform = 'rotate(45deg)'; | |
| hqEl.style.border = '2px solid #fff'; | |
| hqEl.style.boxShadow = '0 0 20px rgba(255, 50, 50, 0.8)'; | |
| new mapboxgl.Marker({{element: hqEl}}) | |
| .setLngLat([2.396667, 48.913418]) | |
| .setPopup(new mapboxgl.Popup().setHTML('<strong>HQ - VORTEX</strong>')) | |
| .addTo(map); | |
| // Marqueurs Clients (Losanges Bleus) | |
| const clients = {clients_markers}; | |
| clients.forEach(client => {{ | |
| const el = document.createElement('div'); | |
| el.style.width = '20px'; | |
| el.style.height = '20px'; | |
| el.style.background = 'linear-gradient(135deg, #25C1F7 0%, #0080ff 100%)'; | |
| el.style.transform = 'rotate(45deg)'; | |
| el.style.border = '1px solid #fff'; | |
| el.style.boxShadow = '0 0 15px rgba(37, 193, 247, 0.6)'; | |
| el.style.cursor = 'pointer'; | |
| new mapboxgl.Marker({{element: el}}) | |
| .setLngLat([client.lon, client.lat]) | |
| .setPopup(new mapboxgl.Popup().setHTML(`<strong>${{client.id}}</strong><br>${{client.adresse}}`)) | |
| .addTo(map); | |
| // Lignes de connexion (si activé) | |
| {'if (true) {' if show_dist else 'if (false) {'} | |
| map.addLayer({{ | |
| 'id': 'line-' + client.id, | |
| 'type': 'line', | |
| 'source': {{ | |
| 'type': 'geojson', | |
| 'data': {{ | |
| 'type': 'Feature', | |
| 'geometry': {{ | |
| 'type': 'LineString', | |
| 'coordinates': [ | |
| [2.396667, 48.913418], | |
| [client.lon, client.lat] | |
| ] | |
| }} | |
| }} | |
| }}, | |
| 'paint': {{ | |
| 'line-color': '#FFD700', | |
| 'line-width': 2, | |
| 'line-opacity': 0.6 | |
| }} | |
| }}); | |
| }} | |
| }}); | |
| // Zone de cluster (si activé) | |
| {'if (true) {' if show_cluster else 'if (false) {'} | |
| map.addLayer({{ | |
| 'id': 'cluster-zone', | |
| 'type': 'circle', | |
| 'source': {{ | |
| 'type': 'geojson', | |
| 'data': {{ | |
| 'type': 'Feature', | |
| 'geometry': {{ | |
| 'type': 'Point', | |
| 'coordinates': [{df_map['lon'].mean()}, {df_map['lat'].mean()}] | |
| }} | |
| }} | |
| }}, | |
| 'paint': {{ | |
| 'circle-radius': {{ | |
| 'stops': [[0, 0], [20, 300]] | |
| }}, | |
| 'circle-color': '#54BD4B', | |
| 'circle-opacity': 0.3, | |
| 'circle-stroke-width': 2, | |
| 'circle-stroke-color': '#54BD4B' | |
| }} | |
| }}); | |
| }} | |
| }}); | |
| // Contrôles de navigation | |
| map.addControl(new mapboxgl.NavigationControl()); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| # Afficher la carte | |
| st.components.v1.html(mapbox_html, height=600) |