Lbasara commited on
Commit
012e912
·
1 Parent(s): 7ed5606

Revert remote to 0.2.4 while keeping local changes~

Browse files
Files changed (5) hide show
  1. CHANGELOG.md +0 -19
  2. app.py +9 -20
  3. geocache.json +0 -0
  4. routingmat.py +3 -0
  5. tableur.py +52 -44
CHANGELOG.md CHANGED
@@ -6,25 +6,6 @@ The format is based on [Keep a Changelog],
6
  and this project adheres to [Semantic Versioning].
7
 
8
 
9
- ## [0.3.0] - 2026-08-28
10
-
11
- ### Added
12
- * Un changement manuel d'adresse met à jour le géocodage.
13
-
14
- ### Changed
15
- * Type du bouton de téléversement de fichiers
16
-
17
- ### Deprecated
18
-
19
- ### Removed
20
-
21
- ### Fixed
22
- * Tuiles passent de CartoDB (qui requiert clé API) à AlidadeSmooth
23
-
24
- ### Security
25
-
26
-
27
-
28
  ## [0.2.4] - 2026-08-18
29
 
30
  ### Added
 
6
  and this project adheres to [Semantic Versioning].
7
 
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  ## [0.2.4] - 2026-08-18
10
 
11
  ### Added
app.py CHANGED
@@ -17,7 +17,7 @@ from routingmat import fetch_matrices
17
 
18
 
19
 
20
- pn.extension("tabulator", 'plotly')
21
 
22
  wgs84="EPSG:4326"
23
  l93="EPSG:9794"
@@ -68,7 +68,7 @@ def fetch_itinéraires(dffinal):
68
  crs=wgs84)
69
  r=requests.get(url="https://data.geopf.fr/navigation/itineraire",
70
  params=params)
71
- sleep(0.1)
72
  if r.ok:
73
  dres[igp]=r.json()
74
 
@@ -77,6 +77,7 @@ def fetch_itinéraires(dffinal):
77
 
78
  def carte(dffinal, dres, dfadr):
79
  tiles='Stadia.AlidadeSmooth'
 
80
 
81
  bbox=dfadr.total_bounds #[0.036343, 48.41117, 0.109909, 48.465001]
82
  m = folium.Map(location=[(bbox[3]+bbox[1])/2, (bbox[2]+bbox[0])/2], tiles=tiles)#, zoom_start=10)
@@ -107,21 +108,6 @@ tabp=Tableur(idx_offset=1001, delivery_tab=tabd)
107
 
108
 
109
 
110
- def tableau_resultats(dffinal):
111
- tab_final=pn.widgets.Tabulator(dffinal.drop("geometry", axis=1).value_counts().reset_index().sort_values(by=["num_vehicule", "heure"]).reset_index(drop=True),
112
- formatters={"heure" : DateFormatter(format="%H:%M")},
113
- disabled=True,
114
- height=1000)
115
-
116
- fichier_exp, bouton_exp = tab_final.download_menu(
117
- text_kwargs={'label': 'Nom csv résultats', 'value': 'Résultats.csv'},
118
- button_kwargs={'label': 'Télécharger'}
119
- )
120
- return pn.Column(
121
- pn.Row(fichier_exp, bouton_exp),
122
- tab_final.style.map(lambda v: f'background-color:{pal[v]};color:white', subset="num_vehicule")
123
- )
124
-
125
 
126
  def tous_calculs(_):
127
 
@@ -150,8 +136,11 @@ def tous_calculs(_):
150
 
151
 
152
  dffinal=mise_en_forme_dfinal(sol, gdfadr)
153
-
154
- tabs.append(('Tableau des résultats', tableau_resultats(dffinal)))
 
 
 
155
 
156
  dres=fetch_itinéraires(dffinal)
157
  m=carte(dffinal, dres, gdfadr)
@@ -167,5 +156,5 @@ tabs=pn.Tabs(
167
 
168
  template= pn.template.MaterialTemplate(title="Problème de tournée des véhicules avec capacités et fenêtre temporelle")
169
  template.main.append(tabs)
170
- template.header.append("v0.3.0")
171
  template.servable()
 
17
 
18
 
19
 
20
+ pn.extension("tabulator", 'filedropper', 'plotly')
21
 
22
  wgs84="EPSG:4326"
23
  l93="EPSG:9794"
 
68
  crs=wgs84)
69
  r=requests.get(url="https://data.geopf.fr/navigation/itineraire",
70
  params=params)
71
+ sleep(0.15)
72
  if r.ok:
73
  dres[igp]=r.json()
74
 
 
77
 
78
  def carte(dffinal, dres, dfadr):
79
  tiles='Stadia.AlidadeSmooth'
80
+ tiles='Cartodb Positron'
81
 
82
  bbox=dfadr.total_bounds #[0.036343, 48.41117, 0.109909, 48.465001]
83
  m = folium.Map(location=[(bbox[3]+bbox[1])/2, (bbox[2]+bbox[0])/2], tiles=tiles)#, zoom_start=10)
 
108
 
109
 
110
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  def tous_calculs(_):
113
 
 
136
 
137
 
138
  dffinal=mise_en_forme_dfinal(sol, gdfadr)
139
+ tab_final=pn.widgets.Tabulator(dffinal.drop("geometry", axis=1).value_counts().reset_index().sort_values(by=["num_vehicule", "heure"]).reset_index(drop=True),
140
+ formatters={"heure" : DateFormatter(format="%H:%M")},
141
+ disabled=True,
142
+ height=1000)
143
+ tabs.append(('Tableau des résultats', tab_final.style.map(lambda v: f'background-color:{pal[v]};color:white', subset="num_vehicule")))
144
 
145
  dres=fetch_itinéraires(dffinal)
146
  m=carte(dffinal, dres, gdfadr)
 
156
 
157
  template= pn.template.MaterialTemplate(title="Problème de tournée des véhicules avec capacités et fenêtre temporelle")
158
  template.main.append(tabs)
159
+ template.header.append("v0.2.4")
160
  template.servable()
geocache.json ADDED
The diff for this file is too large to render. See raw diff
 
routingmat.py CHANGED
@@ -4,6 +4,9 @@ from dotenv import load_dotenv
4
  import os
5
 
6
 
 
 
 
7
  def fetch_matrices(dfadr):
8
 
9
  load_dotenv()
 
4
  import os
5
 
6
 
7
+
8
+
9
+
10
  def fetch_matrices(dfadr):
11
 
12
  load_dotenv()
tableur.py CHANGED
@@ -3,8 +3,7 @@ import dateparser
3
  import numpy as np
4
  import panel as pn
5
  import pandas as pd
6
- from shapely import Point, from_geojson
7
- import json
8
 
9
  from bokeh.models.widgets.tables import DateFormatter
10
 
@@ -12,6 +11,35 @@ from io import BytesIO, StringIO
12
  from datetime import timedelta
13
 
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  class Tableur():
17
  def __init__(self, idx_offset=0, delivery_tab=None):
@@ -22,33 +50,31 @@ class Tableur():
22
  self.dmime={'csv': 'text/csv', 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}
23
  self.feuille=pn.widgets.Select(name='Feuille de calcul', disabled=True, width=200)
24
  self.dest_col=pn.widgets.Select(name="Colonne identifiant la destination", disabled=True, width=200)
25
- self.adr_col=pn.widgets.MultiChoice(name="Colonnes composant l'adresse", disabled=True, width=250,
26
- placeholder="Ex: adresse + CP + ville")
27
- self.tableau=pn.widgets.Tabulator(height=1000,
28
  formatters={"arrivée_max" : DateFormatter(format="%H:%M")},
29
  editors={'arrivée_max': 'datetime'},
30
  widths={'arrivée_max': 160},
31
  page_size=50,
32
  )
33
- self.display_cols=[]
34
- self.file_dropper = pn.widgets.FileInput(accept='.csv, .xlsx', multiple=False)
35
- self.bouton=pn.widgets.Button(name='Géocoder adresses', button_type='primary', on_click=self.lancer_calculs)
36
 
37
  self.file_dropper.param.watch(self.update_dataframes, 'value')
38
  self.feuille.param.watch(self.adr_change, 'value')
39
- self.tableau.on_edit(lambda e: self.recompute_specific_cell(e.column, e.row, e.old, e.value))
40
 
41
 
42
  def update_dataframes(self, _):
43
  self.dfeuilles = {}
44
- file=self.file_dropper
45
- if file.mime_type == self.dmime['xlsx']:
46
- for ks, vs in pd.read_excel(BytesIO(file.value), sheet_name=None).items():
47
- self.dfeuilles[f'{ks}'] = vs
48
- #print("Excel ->", list(dfeuilles.keys()))
49
- else:
50
- self.dfeuilles[0]=pd.read_csv(BytesIO(file.value))
51
- #print("CSV ->", list(dfeuilles.keys()))
52
  if len(self.dfeuilles)>0:
53
  self.feuille.options = list(self.dfeuilles.keys())
54
  self.feuille.value = self.feuille.options[0] # sélectionner la première feuille
@@ -56,24 +82,6 @@ class Tableur():
56
  else:
57
  self.feuille.options=[]
58
  self.feuille.disabled=True
59
-
60
- def recompute_specific_cell(self, col, row, vieux, nouveau):
61
- adr_col=self.adr_col.value
62
- if col not in adr_col:
63
- return
64
- idx=self.df.index[row]
65
- self.df.loc[idx, col]=nouveau
66
- q=" ".join(str(self.df.loc[idx, c]) for c in adr_col)
67
- r=requests.get( url='https://data.geopf.fr/geocodage/search/',
68
- params={"q": q, "limit": 1})
69
- if not r.ok:
70
- return
71
- features=r.json()['features'][0]
72
-
73
- self.df.loc[idx, "suggestion"]=features["properties"]['label']
74
- self.df.loc[idx, "score"]=features["properties"]['score']
75
- self.df.loc[idx, "geometry"]=from_geojson(json.dumps(features['geometry']))
76
- self.tableau.value=self.df[self.display_cols]
77
 
78
  def adr_change(self, _):
79
  self.df = self.dfeuilles.get(self.feuille.value)
@@ -95,36 +103,36 @@ class Tableur():
95
  .rename(columns={"result_score": "score", "result_label": "suggestion"})
96
  dfr["geometry"] = [Point(r["longitude"], r["latitude"]) for _, r in dfr.iterrows()]
97
  self.df=pd.concat([self.df, dfr[["geometry", "score", "suggestion"]]], axis=1)
98
-
99
 
100
  def lancer_calculs(self, _):
101
  self.batch_geocode()
102
- self.display_cols= self.adr_col.value + ["suggestion", "score", self.dest_col.value, "arrivée_max"]
103
  if self.delivery_tab is None:
104
  auj9=dateparser.parse("aujourd'hui 9:00")
105
  self.df["arrivée_max"] = (auj9+ (self.df[self.dest_col.value].str.split().str[0].isin({'Collège', 'Lycée'}))*timedelta(hours=-0.5))
106
  else:
107
- self.delivery_tab.df["arrivée_max"]=self.delivery_tab.tableau.value["arrivée_max"]
108
  self.df=self.df.merge(
109
  self.delivery_tab.df[[self.delivery_tab.dest_col.value, "geometry", "arrivée_max"]].reset_index(names="indice_destination"),
110
  left_on=self.dest_col.value, right_on=self.delivery_tab.dest_col.value,
111
  how="left", suffixes=["_p", "_d"]
112
  )
113
- self.display_cols+=["indice_destination"]
114
  self.df.index = self.idx_offset+np.arange(len(self.df))
115
- self.tableau.value=self.df[self.display_cols]
116
- self.tableau.style.background_gradient(subset=["score"], cmap="RdYlGn", vmin=0.5, vmax=1.0)\
117
  .map(lambda v: '' if v == v else 'background-color:grey;', subset=["indice_destination", "arrivée_max"])
118
- self.tableau.param.trigger("value")
119
  return None
120
 
121
 
122
  def get_panel(self):
123
- return pn.Column(pn.Row(self.file_dropper,
124
  self.feuille,
125
  self.dest_col,
126
  self.adr_col,
127
  self.bouton,
128
  height=100),
129
- self.tableau
130
  )
 
3
  import numpy as np
4
  import panel as pn
5
  import pandas as pd
6
+ from shapely import Point
 
7
 
8
  from bokeh.models.widgets.tables import DateFormatter
9
 
 
11
  from datetime import timedelta
12
 
13
 
14
+ def frenchdrop(dropper: pn.widgets.FileDropper):
15
+ """Example of a FileDropper with French labels via JS injection."""
16
+ js_locale = r"""
17
+ (function(){
18
+ function setFrenchLabels() {
19
+ if (window.FilePond) {
20
+ FilePond.setOptions({
21
+ labelIdle: 'Glissez-déposez un fichier ou <span class="filepond--label-action">Parcourez</span>',
22
+ labelFileProcessing: 'Téléversement en cours',
23
+ labelFileProcessingComplete: 'Téléversement terminé',
24
+ labelFileProcessingAborted: 'Téléversement annulé',
25
+ labelFileProcessingError: 'Erreur pendant le téléversement',
26
+ labelTapToCancel: 'Appuyez pour annuler',
27
+ labelTapToRetry: 'Appuyez pour réessayer',
28
+ labelTapToUndo: 'Appuyez pour annuler',
29
+ labelFileLoading: 'Chargement...',
30
+ labelFileLoadError: 'Erreur de chargement',
31
+ labelFileProcessingRevertError: 'Erreur d’annulation'
32
+ });
33
+ console.log("✅ FilePond traduit en français !");
34
+ } else {
35
+ setTimeout(setFrenchLabels, 200);
36
+ }
37
+ }
38
+ setFrenchLabels();
39
+ })();
40
+ """
41
+ return pn.Column(dropper, pn.pane.HTML(f"<script>{js_locale}</script>"))
42
+
43
 
44
  class Tableur():
45
  def __init__(self, idx_offset=0, delivery_tab=None):
 
50
  self.dmime={'csv': 'text/csv', 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}
51
  self.feuille=pn.widgets.Select(name='Feuille de calcul', disabled=True, width=200)
52
  self.dest_col=pn.widgets.Select(name="Colonne identifiant la destination", disabled=True, width=200)
53
+ self.adr_col=pn.widgets.MultiChoice(name="Colonnes composant l'adresse", disabled=True, width=250)
54
+ self.tabd=pn.widgets.Tabulator(height=1000,
 
55
  formatters={"arrivée_max" : DateFormatter(format="%H:%M")},
56
  editors={'arrivée_max': 'datetime'},
57
  widths={'arrivée_max': 160},
58
  page_size=50,
59
  )
60
+ self.file_dropper = pn.widgets.FileDropper(name="Importer un fichier",
61
+ accepted_filetypes=list(self.dmime.values()))
62
+ self.bouton=pn.widgets.Button(name='Lancer calculs', button_type='primary', on_click=self.lancer_calculs)
63
 
64
  self.file_dropper.param.watch(self.update_dataframes, 'value')
65
  self.feuille.param.watch(self.adr_change, 'value')
 
66
 
67
 
68
  def update_dataframes(self, _):
69
  self.dfeuilles = {}
70
+ for kf, vf in self.file_dropper.value.items():
71
+ if self.file_dropper.mime_type[kf] == self.dmime['xlsx']:
72
+ for ks, vs in pd.read_excel(BytesIO(vf), sheet_name=None).items():
73
+ self.dfeuilles[f'{kf}/{ks}'] = vs
74
+ #print("Excel ->", list(dfeuilles.keys()))
75
+ else:
76
+ self.dfeuilles[kf]=pd.read_csv(StringIO(vf))
77
+ #print("CSV ->", list(dfeuilles.keys()))
78
  if len(self.dfeuilles)>0:
79
  self.feuille.options = list(self.dfeuilles.keys())
80
  self.feuille.value = self.feuille.options[0] # sélectionner la première feuille
 
82
  else:
83
  self.feuille.options=[]
84
  self.feuille.disabled=True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
  def adr_change(self, _):
87
  self.df = self.dfeuilles.get(self.feuille.value)
 
103
  .rename(columns={"result_score": "score", "result_label": "suggestion"})
104
  dfr["geometry"] = [Point(r["longitude"], r["latitude"]) for _, r in dfr.iterrows()]
105
  self.df=pd.concat([self.df, dfr[["geometry", "score", "suggestion"]]], axis=1)
106
+
107
 
108
  def lancer_calculs(self, _):
109
  self.batch_geocode()
110
+ display_cols= self.adr_col.value + ["suggestion", "score", self.dest_col.value, "arrivée_max"]
111
  if self.delivery_tab is None:
112
  auj9=dateparser.parse("aujourd'hui 9:00")
113
  self.df["arrivée_max"] = (auj9+ (self.df[self.dest_col.value].str.split().str[0].isin({'Collège', 'Lycée'}))*timedelta(hours=-0.5))
114
  else:
115
+ self.delivery_tab.df["arrivée_max"]=self.delivery_tab.tabd.value["arrivée_max"]
116
  self.df=self.df.merge(
117
  self.delivery_tab.df[[self.delivery_tab.dest_col.value, "geometry", "arrivée_max"]].reset_index(names="indice_destination"),
118
  left_on=self.dest_col.value, right_on=self.delivery_tab.dest_col.value,
119
  how="left", suffixes=["_p", "_d"]
120
  )
121
+ display_cols+=["indice_destination"]
122
  self.df.index = self.idx_offset+np.arange(len(self.df))
123
+ self.tabd.value=self.df[display_cols]
124
+ self.tabd.style.background_gradient(subset=["score"], cmap="RdYlGn", vmin=0.5, vmax=1.0)\
125
  .map(lambda v: '' if v == v else 'background-color:grey;', subset=["indice_destination", "arrivée_max"])
126
+ self.tabd.param.trigger("value")
127
  return None
128
 
129
 
130
  def get_panel(self):
131
+ return pn.Column(pn.Row(frenchdrop(self.file_dropper),
132
  self.feuille,
133
  self.dest_col,
134
  self.adr_col,
135
  self.bouton,
136
  height=100),
137
+ self.tabd
138
  )