Vortex-Flux / src /modules /map_dashboard.py
klydekushy's picture
Update src/modules/map_dashboard.py
4dbcdf0 verified
raw
history blame
11.2 kB
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)