Spaces:
Sleeping
Sleeping
Revert remote to 0.2.4 while keeping local changes~
Browse files- CHANGELOG.md +0 -19
- app.py +9 -20
- geocache.json +0 -0
- routingmat.py +3 -0
- 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.
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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
|
| 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 |
-
|
| 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.
|
| 34 |
-
|
| 35 |
-
self.bouton=pn.widgets.Button(name='
|
| 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 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 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 |
-
|
| 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.
|
| 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 |
-
|
| 114 |
self.df.index = self.idx_offset+np.arange(len(self.df))
|
| 115 |
-
self.
|
| 116 |
-
self.
|
| 117 |
.map(lambda v: '' if v == v else 'background-color:grey;', subset=["indice_destination", "arrivée_max"])
|
| 118 |
-
self.
|
| 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.
|
| 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 |
)
|