Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
5ebe8a3
Deleted the tests that werent compatible with carpool, down from 58% …
Sep 26, 2025
562c3d9
Added only previous working tests and asset.py test up to 100% covera…
Sep 29, 2025
33e64bd
Added set_params.py tests up to 100% and 26% global coverage
Sep 29, 2025
4918a1b
Added tests for population.py up to 100% coverage, 27% global coverag…
Sep 29, 2025
afe0a0a
Corrected tests for asset.py that crashed because of similar fixture …
Sep 29, 2025
779b7a8
Tests for trips.py at 100% coverage, global coverage up to 29%
Sep 29, 2025
f5e69bb
Tests for transport_zones.py added up to 100% coverage and 30% total …
Sep 29, 2025
ce017f2
Merge pull request #2 from adam-benyekkou/carpool-tests
adam-benyekkou Oct 3, 2025
d4649c3
WIP - Creation de l'interface de base mobility web - Header - Footer …
Oct 7, 2025
56a9056
Nettoyage / déplacement des comments
Oct 7, 2025
923170a
Modification du script d'installation pour la prise en charge de Linu…
Oct 14, 2025
fca5e7c
Changement des r_script et install_r_packages pour rester confirme à …
Oct 15, 2025
666ebb1
Corrected conflict on test_004 for PR
Oct 15, 2025
bcb5178
Corrected conflict on test_004 for PR missing space
Oct 15, 2025
220a41d
Merge pull request #3 from adam-benyekkou/dash-interface-mvp
adam-benyekkou Oct 15, 2025
eb30a80
Reajout du composant study_aread_summary.py qui avait disparu à cause…
Oct 15, 2025
aa91a79
feat - Ajout du composant ScenarioControl contenant pour l'instant un…
Oct 15, 2025
45d4e43
Ajout de deux panneaux latéraux, un avec les stats globales et une lé…
Oct 16, 2025
e2d9c15
Refactorisation des composants Map, StudyAreaSummary et Scenario_Cont…
Oct 17, 2025
110d518
fix - Le service map n'affichait pas le nouveau scénario en cas de cl…
Oct 17, 2025
a8f4cd7
Test d'intégration pour main et deux tests unitaires pour les geo uti…
Oct 17, 2025
27b1330
Ajout de tests pour main, 100% de coverage atteint
Oct 20, 2025
127552e
Changed host in app.run to reflect the real used host
Oct 20, 2025
190f7d6
Harmonizing files between branches
Oct 22, 2025
4258a57
Restore tests/ folder from dash-interface-mvp
Oct 22, 2025
41374ff
Restore tests/ folder from dash-interface-mvp
Oct 22, 2025
360a9b9
Merge pull request #4 from adam-benyekkou/dash-interface-query-form
adam-benyekkou Oct 22, 2025
8aa3483
Modified conftest to reflect main conftest
Oct 23, 2025
c5719c9
Merge pull request #6 from adam-benyekkou/dash-interface-mvp
adam-benyekkou Oct 23, 2025
17943d2
Edited pyproject.toml to match main
Oct 23, 2025
60b9733
Harmonized tests to match main
Oct 23, 2025
7cbcd5b
Solving conflicts on some mobility files
Oct 23, 2025
434e309
Merge branch '192--merge' into dash-interface-query-form
adam-benyekkou Oct 24, 2025
c3d3fb7
Merge branch 'main' into dash-interface-query-form
adam-benyekkou Oct 27, 2025
aa011d2
Added dash dependencies to pyproject.toml for CI testing and easier i…
Oct 27, 2025
88432ae
Pulled from remote
Oct 27, 2025
6e0305a
Moved one dependency from dash testing
Oct 27, 2025
d506518
Adding init.py in front folder for tests
Oct 27, 2025
8bef24f
Adding python paath to pyproject.toml for Ci tests
Oct 27, 2025
16b25d8
Merge branch 'mobility-team:main' into main
adam-benyekkou Oct 28, 2025
8e117b7
Merge branch 'main' into dash-interface-query-form
adam-benyekkou Oct 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 0 additions & 30 deletions DESCRIPTION

This file was deleted.

Binary file removed diff.patch
Binary file not shown.
File renamed without changes.
4 changes: 4 additions & 0 deletions front/app/components/features/map/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Réexporte Map depuis la nouvelle implémentation.
from .map_component import Map

__all__ = ["Map"]
40 changes: 40 additions & 0 deletions front/app/components/features/map/color_scale.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from dataclasses import dataclass
import numpy as np
import pandas as pd

@dataclass(frozen=True)
class ColorScale:
vmin: float
vmax: float

def _rng(self) -> float:
r = self.vmax - self.vmin
return r if r > 1e-9 else 1.0

def legend(self, v) -> str:
if pd.isna(v):
return "Donnée non disponible"
rng = self._rng()
t1 = self.vmin + rng / 3.0
t2 = self.vmin + 2 * rng / 3.0
v = float(v)
if v <= t1:
return "Accès rapide"
if v <= t2:
return "Accès moyen"
return "Accès lent"

def rgba(self, v) -> list[int]:
if pd.isna(v):
return [200, 200, 200, 140]
z = (float(v) - self.vmin) / self._rng()
z = max(0.0, min(1.0, z))
r = int(255 * z)
g = int(64 + 128 * (1 - z))
b = int(255 * (1 - z))
return [r, g, b, 180]

def fit_color_scale(series: pd.Series) -> ColorScale:
s = pd.to_numeric(series, errors="coerce").replace([np.inf, -np.inf], np.nan).dropna()
vmin, vmax = (float(s.min()), float(s.max())) if not s.empty else (0.0, 1.0)
return ColorScale(vmin=vmin, vmax=vmax)
73 changes: 73 additions & 0 deletions front/app/components/features/map/components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import dash_deck
from dash import html
import dash_mantine_components as dmc

from .config import HEADER_OFFSET_PX, SIDEBAR_WIDTH
from app.components.features.study_area_summary import StudyAreaSummary
from app.components.features.scenario_controls import ScenarioControlsPanel

from .tooltip import default_tooltip

def DeckMap(id_prefix: str, deck_json: str) -> dash_deck.DeckGL:
return dash_deck.DeckGL(
id=f"{id_prefix}-deck-map",
data=deck_json,
tooltip=default_tooltip(),
mapboxKey="",
style={
"position": "absolute",
"inset": 0,
"height": "100vh",
"width": "100%",
},
)

def SummaryPanelWrapper(zones_gdf, id_prefix: str):
return html.Div(
id=f"{id_prefix}-summary-wrapper",
children=StudyAreaSummary(zones_gdf, visible=True, id_prefix=id_prefix),
)

def ControlsSidebarWrapper(id_prefix: str):
return html.Div(
dmc.Paper(
children=[
dmc.Stack(
[
ScenarioControlsPanel(
id_prefix=id_prefix,
min_radius=15,
max_radius=50,
step=1,
default=40,
default_insee="31555",
)
],
gap="md",
)
],
withBorder=True,
shadow="md",
radius="md",
p="md",
style={
"width": "100%",
"height": "100%",
"overflowY": "auto",
"overflowX": "hidden",
"background": "#ffffffee",
"boxSizing": "border-box",
},
),
id=f"{id_prefix}-controls-sidebar",
style={
"position": "absolute",
"top": f"{HEADER_OFFSET_PX}px",
"left": "0px",
"bottom": "0px",
"width": f"{SIDEBAR_WIDTH}px",
"zIndex": 1200,
"pointerEvents": "auto",
"overflow": "hidden",
},
)
16 changes: 16 additions & 0 deletions front/app/components/features/map/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from dataclasses import dataclass

# ---------- CONSTANTES ----------
CARTO_POSITRON_GL = "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
FALLBACK_CENTER = (1.4442, 43.6045) # Toulouse

HEADER_OFFSET_PX = 80
SIDEBAR_WIDTH = 340

# ---------- OPTIONS ----------
@dataclass(frozen=True)
class DeckOptions:
zoom: float = 10
pitch: float = 35
bearing: float = -15
map_style: str = CARTO_POSITRON_GL
46 changes: 46 additions & 0 deletions front/app/components/features/map/deck_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import pydeck as pdk
import pandas as pd
import geopandas as gpd

from .config import FALLBACK_CENTER, DeckOptions
from .geo_utils import safe_center
from .color_scale import fit_color_scale
from .layers import build_zones_layer, build_flows_layer

def make_layers(zones_gdf: gpd.GeoDataFrame, flows_df: pd.DataFrame, zones_lookup: gpd.GeoDataFrame):
scale = fit_color_scale(zones_gdf.get("average_travel_time", pd.Series(dtype="float64")))
layers = []
zl = build_zones_layer(zones_gdf, scale)
if zl is not None:
layers.append(zl)
fl = build_flows_layer(flows_df, zones_lookup)
if fl is not None:
layers.append(fl)
return layers

def make_deck(scn: dict, opts: DeckOptions) -> pdk.Deck:
zones_gdf: gpd.GeoDataFrame = scn["zones_gdf"].copy()
flows_df: pd.DataFrame = scn["flows_df"].copy()
zones_lookup: gpd.GeoDataFrame = scn["zones_lookup"].copy()

layers = make_layers(zones_gdf, flows_df, zones_lookup)
lon, lat = safe_center(zones_gdf) or FALLBACK_CENTER

view_state = pdk.ViewState(
longitude=lon,
latitude=lat,
zoom=opts.zoom,
pitch=opts.pitch,
bearing=opts.bearing,
)

return pdk.Deck(
layers=layers,
initial_view_state=view_state,
map_provider="carto",
map_style=opts.map_style,
views=[pdk.View(type="MapView", controller=True)],
)

def make_deck_json(scn: dict, opts: DeckOptions) -> str:
return make_deck(scn, opts).to_json()
62 changes: 62 additions & 0 deletions front/app/components/features/map/geo_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import logging
from typing import Optional, Tuple

import geopandas as gpd
import numpy as np
import pandas as pd
from shapely.geometry import Polygon, MultiPolygon

logger = logging.getLogger(__name__)

def ensure_wgs84(gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
"""Assure EPSG:4326 pour la sortie."""
g = gdf.copy()
if g.crs is None:
g = g.set_crs(4326, allow_override=True)
elif getattr(g.crs, "to_epsg", lambda: None)() != 4326:
g = g.to_crs(4326)
return g

def centroids_lonlat(gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
"""Ajoute colonnes lon/lat calculées en mètres (EPSG:3857) puis reprojetées en 4326."""
g = gdf.copy()
if g.crs is None:
g = g.set_crs(4326, allow_override=True)
g_m = g.to_crs(3857)
pts_m = g_m.geometry.centroid
pts_ll = gpd.GeoSeries(pts_m, crs=g_m.crs).to_crs(4326)
g["lon"] = pts_ll.x.astype("float64")
g["lat"] = pts_ll.y.astype("float64")
return g

def safe_center(gdf: gpd.GeoDataFrame) -> Optional[Tuple[float, float]]:
"""Calcule un centroïde global robuste en WGS84, sinon None."""
try:
zvalid = gdf[gdf.geometry.notnull() & gdf.geometry.is_valid]
if zvalid.empty:
return None
centroid = ensure_wgs84(zvalid).geometry.unary_union.centroid
return float(centroid.x), float(centroid.y)
except Exception as e:
logger.warning("safe_center failed: %s", e)
return None

def fmt_num(v, nd=1):
try:
return f"{round(float(v), nd):.{nd}f}"
except Exception:
return "N/A"

def fmt_pct(v, nd=1):
try:
return f"{round(float(v) * 100.0, nd):.{nd}f} %"
except Exception:
return "N/A"

def as_polygon_rings(geom):
"""Retourne les anneaux extérieurs d’un Polygon/MultiPolygon sous forme de liste de coordonnées."""
if isinstance(geom, Polygon):
return [list(geom.exterior.coords)]
if isinstance(geom, MultiPolygon):
return [list(p.exterior.coords) for p in geom.geoms]
return []
94 changes: 94 additions & 0 deletions front/app/components/features/map/layers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from typing import List, Dict
import pandas as pd
import numpy as np
import pydeck as pdk
import geopandas as gpd

from .geo_utils import ensure_wgs84, as_polygon_rings, fmt_num, fmt_pct, centroids_lonlat
from .color_scale import ColorScale

def _polygons_records(zones_gdf: gpd.GeoDataFrame, scale: ColorScale) -> List[Dict]:
g = ensure_wgs84(zones_gdf)
out = []
for _, row in g.iterrows():
rings = as_polygon_rings(row.geometry)
if not rings:
continue

zone_id = row.get("transport_zone_id", "Zone inconnue")
insee = row.get("local_admin_unit_id", "N/A")
avg_tt = pd.to_numeric(row.get("average_travel_time", np.nan), errors="coerce")
total_dist_km = pd.to_numeric(row.get("total_dist_km", np.nan), errors="coerce")
total_time_min = pd.to_numeric(row.get("total_time_min", np.nan), errors="coerce")
share_car = pd.to_numeric(row.get("share_car", np.nan), errors="coerce")
share_bicycle = pd.to_numeric(row.get("share_bicycle", np.nan), errors="coerce")
share_walk = pd.to_numeric(row.get("share_walk", np.nan), errors="coerce")

for ring in rings:
out.append({
"geometry": [[float(x), float(y)] for x, y in ring],
"fill_rgba": scale.rgba(avg_tt),
"Unité INSEE": str(insee),
"Identifiant de zone": str(zone_id),
"Temps moyen de trajet (minutes)": fmt_num(avg_tt, 1),
"Niveau d’accessibilité": scale.legend(avg_tt),
"Distance totale parcourue (km/jour)": fmt_num(total_dist_km, 1),
"Temps total de déplacement (min/jour)": fmt_num(total_time_min, 1),
"Part des trajets en voiture (%)": fmt_pct(share_car, 1),
"Part des trajets à vélo (%)": fmt_pct(share_bicycle, 1),
"Part des trajets à pied (%)": fmt_pct(share_walk, 1),
})
return out

def build_zones_layer(zones_gdf: gpd.GeoDataFrame, scale: ColorScale) -> pdk.Layer | None:
polys = _polygons_records(zones_gdf, scale)
if not polys:
return None
return pdk.Layer(
"PolygonLayer",
data=polys,
get_polygon="geometry",
get_fill_color="fill_rgba",
pickable=True,
filled=True,
stroked=True,
get_line_color=[0, 0, 0, 80],
lineWidthMinPixels=1.5,
elevation_scale=0,
opacity=0.4,
auto_highlight=True,
)

def build_flows_layer(flows_df: pd.DataFrame, zones_lookup: gpd.GeoDataFrame) -> pdk.Layer | None:
if flows_df is None or flows_df.empty:
return None

lookup_ll = centroids_lonlat(zones_lookup)
f = flows_df.copy()
f["flow_volume"] = pd.to_numeric(f["flow_volume"], errors="coerce").fillna(0.0)
f = f[f["flow_volume"] > 0]

f = f.merge(
lookup_ll[["transport_zone_id", "lon", "lat"]],
left_on="from", right_on="transport_zone_id", how="left"
).rename(columns={"lon": "lon_from", "lat": "lat_from"}).drop(columns=["transport_zone_id"])
f = f.merge(
lookup_ll[["transport_zone_id", "lon", "lat"]],
left_on="to", right_on="transport_zone_id", how="left"
).rename(columns={"lon": "lon_to", "lat": "lat_to"}).drop(columns=["transport_zone_id"])
f = f.dropna(subset=["lon_from", "lat_from", "lon_to", "lat_to"])
if f.empty:
return None

f["flow_width"] = (1.0 + np.log1p(f["flow_volume"])).astype("float64").clip(0.5, 6.0)

return pdk.Layer(
"ArcLayer",
data=f,
get_source_position=["lon_from", "lat_from"],
get_target_position=["lon_to", "lat_to"],
get_source_color=[255, 140, 0, 180],
get_target_color=[0, 128, 255, 180],
get_width="flow_width",
pickable=True,
)
Loading
Loading