From bd6a4ff2660f2a1503b6f0de279ea091d41abbe3 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Tue, 28 Apr 2026 10:40:50 +0200 Subject: [PATCH] =?UTF-8?q?Tentative=20de=20r=C3=A9tablissement=20de=20la?= =?UTF-8?q?=20carte=20sur=20acheteur/titulaire?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plans/2026-04-28-organization-maps.md | 108 ++++++++++++++++++ pyproject.toml | 3 +- run.py | 4 +- src/figures.py | 86 ++++++++++---- src/pages/acheteur.py | 9 +- src/pages/marche.py | 2 +- src/pages/titulaire.py | 9 +- src/utils/table.py | 2 + uv.lock | 15 +++ 9 files changed, 210 insertions(+), 28 deletions(-) create mode 100644 docs/superpowers/plans/2026-04-28-organization-maps.md diff --git a/docs/superpowers/plans/2026-04-28-organization-maps.md b/docs/superpowers/plans/2026-04-28-organization-maps.md new file mode 100644 index 0000000..4e9e54a --- /dev/null +++ b/docs/superpowers/plans/2026-04-28-organization-maps.md @@ -0,0 +1,108 @@ +# Plan: Ajouter des cartes de localisation aux pages acheteur et titulaire + +## Date: 2026-04-28 + +## Statut: Approuvé + +## Objectif: Ajouter des cartes interactives montrant la localisation des organisations sur les pages acheteur et titulaire + +## Contexte + +- Les pages acheteur et titulaire ont déjà des placeholders pour les cartes (`acheteur_map` et `titulaire_map`) +- La fonction `point_on_map()` existe déjà dans `src/figures.py` mais utilise un centrage fixe sur la France +- Les données de localisation proviennent de l'API Annuaire des Entreprises +- Les codes départementaux sont disponibles et plus fiables que les coordonnées pour la détection de région + +## Exigences + +### 1. Carte interactive + +- **Localisation**: Colonne de droite dans la section d'informations sur l'organisation +- **Taille**: 400px de largeur × 300px de hauteur (fixe) +- **Contenu**: Carte centrée sur la France ou le département d'outre-mer approprié avec un point rouge à l'emplacement de l'organisation +- **Niveau de zoom**: Approprié pour montrer l'Hexagone ou le département d'outre-mer spécifique +- **Style**: Fond de carte clair avec point rouge visible +- **Interactivité**: Carte zoomable et déplaçable (pas de configuration statique) + +### 2. Sources de données + +- Utiliser les colonnes `acheteur_latitude` et `acheteur_longitude` pour les pages acheteur +- Utiliser les colonnes `titulaire_latitude` et `titulaire_longitude` pour les pages titulaire +- Utiliser les codes départementaux (`acheteur_departement_code`, `titulaire_departement_code`) pour la détection de région +- Solution de repli: Si les coordonnées ou codes départementaux sont manquants ou invalides, afficher une div vide + +### 3. Détection de région + +- **Départements métropolitains**: Codes à 2 caractères (ex: "75" pour Paris) → Carte Hexagone +- **Départements d'outre-mer**: + - "971" → Guadeloupe + - "972" → Martinique + - "973" → Guyane + - "974" → La Réunion + - "976" → Mayotte +- **Code département manquant**: Retourner une div vide (pas de détection basée sur les coordonnées) + +### 4. Gestion des erreurs + +- Coordonnées invalides → div vide +- Code département manquant → div vide +- Échec de l'API Annuaire → div vide (comportement existant) +- Format de code département invalide → div vide + +## Implémentation + +### Fichiers à modifier + +#### 1. `src/figures.py` - Améliorer la fonction `point_on_map()` + +**Ligne 178-209**: Remplacer la fonction existante par une version améliorée avec: + +- Détection de région basée sur les codes départementaux +- Configuration de carte interactive (zoomable) +- Point plus grand (size=15) +- Commentaires en français + +#### 2. `src/pages/acheteur.py` - Mettre à jour le callback + +**Ligne 249-297**: Modifier `update_acheteur_infos()` pour: + +- Extraire le code département du code postal +- Passer le code département à `point_on_map()` +- Ajouter des commentaires en français + +#### 3. `src/pages/titulaire.py` - Mettre à jour le callback + +**Ligne 259-297**: Modifier `update_titulaire_infos()` pour: + +- Extraire le code département du code postal +- Passer le code département à `point_on_map()` +- Ajouter des commentaires en français + +## Plan de Test + +### Cas de test prioritaires + +1. **Organisation métropolitaine**: Code département "75" (Paris) → Carte Hexagone +2. **Organisation à La Réunion**: Code département "974" → Carte centrée sur La Réunion +3. **Code département manquant**: Retourne une div vide +4. **Coordonnées invalides**: Retourne une div vide +5. **Interactivité**: Vérifier zoom et déplacement + +### Critères d'acceptation + +- [ ] Cartes fonctionnelles avec codes départementaux valides +- [ ] Div vide pour codes manquants/invalides +- [ ] Cartes correctement centrées et zoomées +- [ ] Interactivité (zoom et déplacement) +- [ ] Point de localisation visible (size=15) + +## Approbation + +Plan approuvé avec spécifications: + +- Réutiliser et améliorer `point_on_map` +- Retourner div vide sans code département +- Point légèrement plus grand +- Cartes zoomables +- Utiliser codes départementaux pour détection de région +- Commentaires en français diff --git a/pyproject.toml b/pyproject.toml index ee90897..873a560 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,13 +14,14 @@ dependencies = [ "xlsxwriter", "plotly[express]", "httpx", - "pandas", # utilisé pour la création de certains graphiques + "pandas", # utilisé pour la création de certains graphiques "unidecode", "dash-leaflet", "dash-extensions", "duckdb", "flask-caching", "pyarrow>=23.0.1", + "flask-cors>=6.0.2", ] [dependency-groups] diff --git a/run.py b/run.py index 215b786..a3e312a 100644 --- a/run.py +++ b/run.py @@ -1,7 +1,9 @@ +from flask_cors import CORS + from src.app import app # To use `gunicorn run:server` (prod) -server = app.server +server = CORS(app.server) # To use `python run.py` (dev) if __name__ == "__main__": diff --git a/src/figures.py b/src/figures.py index 16a36eb..9a2807b 100644 --- a/src/figures.py +++ b/src/figures.py @@ -175,38 +175,78 @@ def get_sources_tables(source_path) -> html.Div: return html.Div(children=datatable) -def point_on_map(lat, lon): - lat = float(lat) - lon = float(lon) +def point_on_map(lat, lon, departement_code=None): + """Fonction améliorée utilisant les codes départementaux pour la détection de région. - # Create a scatter mapbox or choropleth map - fig = px.scatter_map( - lat=[lat], lon=[lon], height=300, width=400, color=[1], size=[1] + Args: + lat: Coordonnée de latitude + lon: Coordonnée de longitude + departement_code: Code du département (ex: '75', '971', etc.) + + Returns: + html.Div contenant la carte, ou div vide si invalide + """ + # Validation des coordonnées + try: + lat = float(lat) + lon = float(lon) + except (TypeError, ValueError): + return html.Div() # Div vide pour les coordonnées invalides + + # Vérification que les coordonnées sont valides + if not (-90 <= lat <= 90) or not (-180 <= lon <= 180): + return html.Div() + + # Si aucun code département n'est fourni, retourner une div vide + if not departement_code: + return html.Div() + + # Détermination de la région en utilisant le code département + # Logique identique à get_geographic_maps + if departement_code in ["971", "972", "973", "974", "976"]: + region_key = departement_code # Département d'outre-mer + elif len(departement_code) == 2: # Département métropolitain + region_key = "Hexagone" + else: + return html.Div() # Format de code département invalide + + # Paramètres de carte par région (réutilisés de get_geographic_maps) + regions = { + "Hexagone": {"center": [46.6, 2.2], "zoom": 5}, + "971": {"center": [16.23, -61.55], "zoom": 9}, # Guadeloupe + "972": {"center": [14.64, -61.02], "zoom": 10}, # Martinique + "973": {"center": [3.93, -53.12], "zoom": 7}, # Guyane + "974": {"center": [-21.11, 55.53], "zoom": 9}, # La Réunion + "976": {"center": [-12.82, 45.16], "zoom": 10}, # Mayotte + } + + settings = regions.get(region_key, regions["Hexagone"]) + + # Création de la carte avec un point plus grand + fig = px.scatter_mapbox( + lat=[lat], + lon=[lon], + height=300, + width=400, + color=[1], + size=[15], # Point plus grand comme demandé + zoom=settings["zoom"], ) - fig.update_coloraxes(showscale=False) - - # Set map style (you can use 'open-street-map', 'carto-positron', etc.) + # Configuration de la carte (interactive - zoomable) fig.update_layout( - mapbox_style="light", # Light, clean background + mapbox_style="light", # Fond de carte clair margin={"r": 0, "t": 0, "l": 0, "b": 0}, + # Note: pas de uirevision='static' pour permettre l'interactivité ) - # Optionally, center the map on France - fig.update_geos( - center=dict(lat=46.603354, lon=1.888334), # Center of France - lataxis_range=[41, 51.5], # Latitude range for France - lonaxis_range=[-5, 10], # Longitude range for France + # Centrage et zoom appropriés + fig.update_layout( + mapbox_center={"lat": settings["center"][0], "lon": settings["center"][1]}, + mapbox_zoom=settings["zoom"], ) - # But scatter_mapbox doesn't use geos, so better to control via zoom/center manually - # Let's reset and use proper centering in scatter_mapbox instead: - - fig.update_layout(map_center={"lat": 46.6, "lon": 1.89}, map_zoom=4) - - graph = dcc.Graph(id="map", figure=fig) - graph = html.Div(style={"width": "400px"}) - return graph + return html.Div(dcc.Graph(figure=fig), style={"width": "400px"}) class DataTable(dash_table.DataTable): diff --git a/src/pages/acheteur.py b/src/pages/acheteur.py index e0e708a..23bf203 100644 --- a/src/pages/acheteur.py +++ b/src/pages/acheteur.py @@ -257,8 +257,15 @@ def update_acheteur_infos(url): if data_etablissement: data_etablissement = data_etablissement[0] + # Extraction du code département à partir du code postal + code_postal = data_etablissement.get("code_postal", "") + departement_code = code_postal[:2] if code_postal else None + + # Création de la carte avec le code département pour un centrage approprié acheteur_map = point_on_map( - data_etablissement["latitude"], data_etablissement["longitude"] + data_etablissement["latitude"], + data_etablissement["longitude"], + departement_code, ) code_departement, nom_departement, nom_region = get_departement_region( data_etablissement["code_postal"] diff --git a/src/pages/marche.py b/src/pages/marche.py index 91786d2..9587b56 100644 --- a/src/pages/marche.py +++ b/src/pages/marche.py @@ -243,7 +243,7 @@ def get_marche_jsonld(marche, titulaires) -> str: titulaire.get("titulaire_id"), org_name=titulaire.get("titulaire_nom"), org_type="titulaire", - type_org_id=titulaire.get("titulaire_typeIdentifiant"), + type_org_id=titulaire.get("titulaire_typeIdentifiant", "SIRET"), ), "orderedItem": { "@type": type_order, diff --git a/src/pages/titulaire.py b/src/pages/titulaire.py index 2a1005c..3edb8ba 100644 --- a/src/pages/titulaire.py +++ b/src/pages/titulaire.py @@ -263,8 +263,15 @@ def update_titulaire_infos(url): if data_etablissement: data_etablissement = data_etablissement[0] + # Extraction du code département à partir du code postal + code_postal = data_etablissement.get("code_postal", "") + departement_code = code_postal[:2] if code_postal else None + + # Création de la carte avec le code département pour un centrage approprié titulaire_map = point_on_map( - data_etablissement["latitude"], data_etablissement["longitude"] + data_etablissement["latitude"], + data_etablissement["longitude"], + departement_code, ) code_departement, nom_departement, nom_region = get_departement_region( data_etablissement["code_postal"] diff --git a/src/utils/table.py b/src/utils/table.py index cb4108e..ef5221c 100644 --- a/src/utils/table.py +++ b/src/utils/table.py @@ -154,6 +154,8 @@ def normalize_sort_by(sort_by) -> tuple: def format_number(number) -> str: + if not number: + return "" number = "{:,}".format(number).replace(",", " ") return number diff --git a/uv.lock b/uv.lock index 783477a..db47a6d 100644 --- a/uv.lock +++ b/uv.lock @@ -771,6 +771,7 @@ dependencies = [ { name = "dash-leaflet", version = "1.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "duckdb" }, { name = "flask-caching" }, + { name = "flask-cors" }, { name = "gunicorn" }, { name = "httpx" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -803,6 +804,7 @@ requires-dist = [ { name = "dash-leaflet" }, { name = "duckdb" }, { name = "flask-caching" }, + { name = "flask-cors", specifier = ">=6.0.2" }, { name = "gunicorn" }, { name = "httpx" }, { name = "pandas" }, @@ -978,6 +980,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4c/0f/fe51e0b2301bbd429af44273a923ff92127b18d13abba5ae5a1d60e8e497/flask_compress-1.24-py3-none-any.whl", hash = "sha256:1e63668eb6e3242bd4f6ad98825a924e3984409be90c125477893d586007d00c", size = 11033 }, ] +[[package]] +name = "flask-cors" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/74/0fc0fa68d62f21daef41017dafab19ef4b36551521260987eb3a5394c7ba/flask_cors-6.0.2.tar.gz", hash = "sha256:6e118f3698249ae33e429760db98ce032a8bf9913638d085ca0f4c5534ad2423", size = 13472 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/af/72ad54402e599152de6d067324c46fe6a4f531c7c65baf7e96c63db55eaf/flask_cors-6.0.2-py3-none-any.whl", hash = "sha256:e57544d415dfd7da89a9564e1e3a9e515042df76e12130641ca6f3f2f03b699a", size = 13257 }, +] + [[package]] name = "geobuf" version = "2.0.1"