Spaces:
Running
Running
File size: 11,186 Bytes
7d8f0ad 8f066dd 7d8f0ad bec67a7 8f066dd bec67a7 8f066dd bec67a7 7d8f0ad bec67a7 7d8f0ad 59055d8 7d8f0ad bec67a7 59055d8 bec67a7 59055d8 7d8f0ad 5cea090 59055d8 7d8f0ad bec67a7 7d8f0ad 59055d8 7d8f0ad cd68b4a 7d8f0ad cd68b4a 7d8f0ad 4dbcdf0 bec67a7 4dbcdf0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | 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) ---
@st.cache_data(show_spinner=False)
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) |