Tentative de rétablissement de la carte sur acheteur/titulaire
This commit is contained in:
@@ -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
|
||||||
@@ -21,6 +21,7 @@ dependencies = [
|
|||||||
"duckdb",
|
"duckdb",
|
||||||
"flask-caching",
|
"flask-caching",
|
||||||
"pyarrow>=23.0.1",
|
"pyarrow>=23.0.1",
|
||||||
|
"flask-cors>=6.0.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
from flask_cors import CORS
|
||||||
|
|
||||||
from src.app import app
|
from src.app import app
|
||||||
|
|
||||||
# To use `gunicorn run:server` (prod)
|
# To use `gunicorn run:server` (prod)
|
||||||
server = app.server
|
server = CORS(app.server)
|
||||||
|
|
||||||
# To use `python run.py` (dev)
|
# To use `python run.py` (dev)
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+61
-21
@@ -175,38 +175,78 @@ def get_sources_tables(source_path) -> html.Div:
|
|||||||
return html.Div(children=datatable)
|
return html.Div(children=datatable)
|
||||||
|
|
||||||
|
|
||||||
def point_on_map(lat, 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.
|
||||||
|
|
||||||
|
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)
|
lat = float(lat)
|
||||||
lon = float(lon)
|
lon = float(lon)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return html.Div() # Div vide pour les coordonnées invalides
|
||||||
|
|
||||||
# Create a scatter mapbox or choropleth map
|
# Vérification que les coordonnées sont valides
|
||||||
fig = px.scatter_map(
|
if not (-90 <= lat <= 90) or not (-180 <= lon <= 180):
|
||||||
lat=[lat], lon=[lon], height=300, width=400, color=[1], size=[1]
|
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)
|
# Configuration de la carte (interactive - zoomable)
|
||||||
|
|
||||||
# Set map style (you can use 'open-street-map', 'carto-positron', etc.)
|
|
||||||
fig.update_layout(
|
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},
|
margin={"r": 0, "t": 0, "l": 0, "b": 0},
|
||||||
|
# Note: pas de uirevision='static' pour permettre l'interactivité
|
||||||
)
|
)
|
||||||
|
|
||||||
# Optionally, center the map on France
|
# Centrage et zoom appropriés
|
||||||
fig.update_geos(
|
fig.update_layout(
|
||||||
center=dict(lat=46.603354, lon=1.888334), # Center of France
|
mapbox_center={"lat": settings["center"][0], "lon": settings["center"][1]},
|
||||||
lataxis_range=[41, 51.5], # Latitude range for France
|
mapbox_zoom=settings["zoom"],
|
||||||
lonaxis_range=[-5, 10], # Longitude range for France
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# But scatter_mapbox doesn't use geos, so better to control via zoom/center manually
|
return html.Div(dcc.Graph(figure=fig), style={"width": "400px"})
|
||||||
# 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
|
|
||||||
|
|
||||||
|
|
||||||
class DataTable(dash_table.DataTable):
|
class DataTable(dash_table.DataTable):
|
||||||
|
|||||||
@@ -257,8 +257,15 @@ def update_acheteur_infos(url):
|
|||||||
if data_etablissement:
|
if data_etablissement:
|
||||||
data_etablissement = data_etablissement[0]
|
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(
|
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(
|
code_departement, nom_departement, nom_region = get_departement_region(
|
||||||
data_etablissement["code_postal"]
|
data_etablissement["code_postal"]
|
||||||
|
|||||||
+1
-1
@@ -243,7 +243,7 @@ def get_marche_jsonld(marche, titulaires) -> str:
|
|||||||
titulaire.get("titulaire_id"),
|
titulaire.get("titulaire_id"),
|
||||||
org_name=titulaire.get("titulaire_nom"),
|
org_name=titulaire.get("titulaire_nom"),
|
||||||
org_type="titulaire",
|
org_type="titulaire",
|
||||||
type_org_id=titulaire.get("titulaire_typeIdentifiant"),
|
type_org_id=titulaire.get("titulaire_typeIdentifiant", "SIRET"),
|
||||||
),
|
),
|
||||||
"orderedItem": {
|
"orderedItem": {
|
||||||
"@type": type_order,
|
"@type": type_order,
|
||||||
|
|||||||
@@ -263,8 +263,15 @@ def update_titulaire_infos(url):
|
|||||||
if data_etablissement:
|
if data_etablissement:
|
||||||
data_etablissement = data_etablissement[0]
|
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(
|
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(
|
code_departement, nom_departement, nom_region = get_departement_region(
|
||||||
data_etablissement["code_postal"]
|
data_etablissement["code_postal"]
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ def normalize_sort_by(sort_by) -> tuple:
|
|||||||
|
|
||||||
|
|
||||||
def format_number(number) -> str:
|
def format_number(number) -> str:
|
||||||
|
if not number:
|
||||||
|
return ""
|
||||||
number = "{:,}".format(number).replace(",", " ")
|
number = "{:,}".format(number).replace(",", " ")
|
||||||
return number
|
return number
|
||||||
|
|
||||||
|
|||||||
@@ -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 = "dash-leaflet", version = "1.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||||
{ name = "duckdb" },
|
{ name = "duckdb" },
|
||||||
{ name = "flask-caching" },
|
{ name = "flask-caching" },
|
||||||
|
{ name = "flask-cors" },
|
||||||
{ name = "gunicorn" },
|
{ name = "gunicorn" },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
{ 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 = "dash-leaflet" },
|
||||||
{ name = "duckdb" },
|
{ name = "duckdb" },
|
||||||
{ name = "flask-caching" },
|
{ name = "flask-caching" },
|
||||||
|
{ name = "flask-cors", specifier = ">=6.0.2" },
|
||||||
{ name = "gunicorn" },
|
{ name = "gunicorn" },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "pandas" },
|
{ 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 },
|
{ 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]]
|
[[package]]
|
||||||
name = "geobuf"
|
name = "geobuf"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user