Upload 4 files
Browse files- README.md +0 -0
- pyproject.toml +27 -0
- tournenrond.py +214 -0
- uv.lock +0 -0
README.md
ADDED
File without changes
|
pyproject.toml
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[project]
|
2 |
+
name = "autoroute"
|
3 |
+
version = "0.1.0"
|
4 |
+
description = "Add your description here"
|
5 |
+
readme = "README.md"
|
6 |
+
requires-python = ">=3.12"
|
7 |
+
dependencies = [
|
8 |
+
"matplotlib>=3.9.2",
|
9 |
+
"networkx>=3.4.2",
|
10 |
+
"osmnx>=2",
|
11 |
+
"pdfplumber>=0.11.4",
|
12 |
+
"plotly>=5.24.1",
|
13 |
+
"pymupdf>=1.24.13",
|
14 |
+
"pymupdf4llm>=0.0.17",
|
15 |
+
"pypdf2<3.0",
|
16 |
+
"pyvis>=0.3.2",
|
17 |
+
"scipy>=1.14.1",
|
18 |
+
"tabula-py>=2.10.0",
|
19 |
+
"tabula>=1.0.5",
|
20 |
+
"scikit-learn>=1.5.2",
|
21 |
+
"jupyterlab>=4.3.1",
|
22 |
+
"folium>=0.18.0",
|
23 |
+
"panel>=1.5.4",
|
24 |
+
"param>=2.1.1",
|
25 |
+
"mapclassify>=2.8.1",
|
26 |
+
"colorcet>=3.1.0",
|
27 |
+
]
|
tournenrond.py
ADDED
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
import osmnx as ox
|
3 |
+
import networkx as nx
|
4 |
+
import folium
|
5 |
+
from networkx.classes.function import path_weight
|
6 |
+
from osmnx.geocoder import geocode
|
7 |
+
import panel as pn
|
8 |
+
import param
|
9 |
+
import colorcet as cc
|
10 |
+
from plotly import graph_objects as go
|
11 |
+
from shapely.ops import linemerge
|
12 |
+
from pyproj import Transformer
|
13 |
+
import pandas as pd
|
14 |
+
import geopandas as gpd
|
15 |
+
from io import BytesIO
|
16 |
+
from time import sleep
|
17 |
+
|
18 |
+
ox.settings.elevation_url_template="https://api.opentopodata.org/v1/aster30m?locations={locations}"
|
19 |
+
pn.extension('plotly')
|
20 |
+
|
21 |
+
class Pars(param.Parameterized):
|
22 |
+
adresse= param.String(label="Adresse", default="Rue du Stade Saint-Ouen-de-Mimbré")
|
23 |
+
bornes = param.Range(label="Distance (km)", bounds=(1, 43), default=(5, 7), step=1)
|
24 |
+
pente=param.Number(label="Pente segment max (%)", bounds=(0, 15), default=10)
|
25 |
+
nresults=param.Integer(label="Number of results", bounds=(1, 20), default=5)
|
26 |
+
precision= param.Number(label="Précision (%)", softbounds=(0, 10), default=1,
|
27 |
+
doc="Plus faible : plus précis, plus élevé : calcul plus rapide\n5 est un bon compromis")
|
28 |
+
|
29 |
+
par=Pars()
|
30 |
+
|
31 |
+
|
32 |
+
def is_feasible(points, p, min_dist):
|
33 |
+
selected_points = [points[0]]
|
34 |
+
last_position = points[0]
|
35 |
+
|
36 |
+
for i in range(1, len(points)):
|
37 |
+
if points[i] - last_position >= min_dist:
|
38 |
+
selected_points.append(points[i])
|
39 |
+
last_position = points[i]
|
40 |
+
if len(selected_points) == p:
|
41 |
+
return True, selected_points
|
42 |
+
return False, []
|
43 |
+
|
44 |
+
def sorted_maxmin_dispersion(points, p):
|
45 |
+
left = 0
|
46 |
+
right = points[-1] - points[0]
|
47 |
+
best_dist = 0
|
48 |
+
best_points = []
|
49 |
+
|
50 |
+
while left <= right:
|
51 |
+
mid = (left + right) // 2
|
52 |
+
feasible, selected_points = is_feasible(points, p, mid)
|
53 |
+
if feasible:
|
54 |
+
best_dist = mid
|
55 |
+
best_points = selected_points
|
56 |
+
left = mid + 1
|
57 |
+
else:
|
58 |
+
right = mid - 1
|
59 |
+
return best_points, best_dist
|
60 |
+
|
61 |
+
def maxmin_dispersion(points, p) :
|
62 |
+
if p>=len(points):
|
63 |
+
return points, 0
|
64 |
+
points.sort()
|
65 |
+
best_points, best_dist=sorted_maxmin_dispersion(points, p)
|
66 |
+
best_points[-1]=points[-1]
|
67 |
+
return best_points, best_dist
|
68 |
+
|
69 |
+
|
70 |
+
def cycle_maxmin_dispersion(points, p, longueur):
|
71 |
+
if p>=len(points):
|
72 |
+
return points, 0
|
73 |
+
points.sort()
|
74 |
+
best_points, best_dist=sorted_maxmin_dispersion( points + [points[0] + longueur], p+1)
|
75 |
+
return best_points[:-1], best_dist
|
76 |
+
|
77 |
+
|
78 |
+
|
79 |
+
|
80 |
+
|
81 |
+
def create_trace(choixtrace):
|
82 |
+
df= dic_dfg[ int(choixtrace)-1 ]
|
83 |
+
dfg = gpd.GeoDataFrame(index=[0], crs='epsg:4326', geometry=[linemerge(df.geometry.to_list())])
|
84 |
+
sio = BytesIO()
|
85 |
+
dfg.to_file(sio, 'GPX')
|
86 |
+
sio.seek(0)
|
87 |
+
return sio
|
88 |
+
|
89 |
+
|
90 |
+
fig = go.Figure()
|
91 |
+
dic_dfg={}
|
92 |
+
calcul=pn.widgets.Button(button_type= "primary", name="Calculer parcours")
|
93 |
+
carte=pn.pane.plot.Folium(min_height=1000, sizing_mode='stretch_both')
|
94 |
+
choixtrace=pn.widgets.Select(name="Sélection trace", visible=False)
|
95 |
+
filedownload = pn.widgets.FileDownload(name="Télécharger GPX", button_type="primary",
|
96 |
+
callback=pn.bind(create_trace, choixtrace), filename='RunLoop.gpx', visible=False
|
97 |
+
)
|
98 |
+
|
99 |
+
elev=pn.pane.Plotly(fig, sizing_mode='stretch_width', visible=False)
|
100 |
+
|
101 |
+
tuiles= pn.widgets.RadioBoxGroup(name='Style carte',
|
102 |
+
options={'Style classique' : 'OpenStreetMap', 'Style clair': 'Cartodb Positron'}, inline=True)
|
103 |
+
|
104 |
+
|
105 |
+
def download_graph(coo):
|
106 |
+
G = ox.graph_from_point(coo, dist=par.bornes[1]*1000/2, dist_type="network", network_type='walk')
|
107 |
+
return G
|
108 |
+
|
109 |
+
def consolidate_intersections(G):
|
110 |
+
Gp= ox.projection.project_graph(G)
|
111 |
+
tolerance=sum(par.bornes)/2 * par.precision *10 # bornes en km, precision en %
|
112 |
+
Gc=ox.simplification.consolidate_intersections(Gp, tolerance=tolerance)
|
113 |
+
return Gc
|
114 |
+
|
115 |
+
|
116 |
+
def filter_elevations(Gc):
|
117 |
+
Gc=ox.projection.project_graph(Gc, to_latlong=True)
|
118 |
+
Ge=ox.elevation.add_edge_grades(ox.elevation.add_node_elevations_google(Gc, batch_size=100, pause=0.5))
|
119 |
+
Gf=Ge.copy()
|
120 |
+
for s, t, d in Ge.edges(data=True):
|
121 |
+
if d['grade_abs']>par.pente/100:
|
122 |
+
Gf.remove_edge(s, t)
|
123 |
+
return Gf
|
124 |
+
|
125 |
+
def get_paths(Gf, lon, lat):
|
126 |
+
source, target, _=ox.distance.nearest_edges(Gf, lon, lat)
|
127 |
+
mindist, maxdist=1000*par.bornes[0], 1000*par.bornes[1]
|
128 |
+
Gfu=ox.convert.to_digraph(Gf).to_undirected()
|
129 |
+
short_paths=nx.shortest_simple_paths(Gfu, source=source, target=target, weight="length")
|
130 |
+
dist=0
|
131 |
+
chemins=[]
|
132 |
+
i=0
|
133 |
+
|
134 |
+
while dist<maxdist:
|
135 |
+
i+=1
|
136 |
+
path=next(short_paths)+[source]
|
137 |
+
dist=path_weight(Gfu, path, weight="length")
|
138 |
+
if dist>mindist:
|
139 |
+
chemins.append((dist, path))
|
140 |
+
|
141 |
+
return chemins
|
142 |
+
|
143 |
+
|
144 |
+
def get_elevation(dfg, Gc):
|
145 |
+
df=pd.DataFrame(linemerge(dfg.geometry.to_list()).coords, columns=["x", "y"])
|
146 |
+
df[["ym", "xm"]]=linemerge(dfg.to_crs(Gc.graph["crs"]).geometry.to_list()).coords
|
147 |
+
df["segdists"]=np.hypot(*(df[["xm", "ym"]].diff().fillna(0).T.values))
|
148 |
+
df["segcumsum"]=np.cumsum(df["segdists"])
|
149 |
+
|
150 |
+
pts, optdist = maxmin_dispersion(df.segcumsum.to_list(), 100)
|
151 |
+
|
152 |
+
df2=df[df['segcumsum'].isin(pts)].copy()
|
153 |
+
Gchemin=nx.MultiDiGraph()
|
154 |
+
Gchemin.add_nodes_from( list(zip (df2.index, df2[["x", "y"]].to_dict('records'))) )
|
155 |
+
Gchemin=ox.elevation.add_node_elevations_google(Gchemin)
|
156 |
+
sleep(0.5)
|
157 |
+
idx, prop= zip(*Gchemin.nodes(data=True))
|
158 |
+
df2["elevation"]=pd.DataFrame(prop, index=idx)["elevation"]
|
159 |
+
return df2
|
160 |
+
|
161 |
+
def calculs(dummy):
|
162 |
+
|
163 |
+
coo=geocode(par.adresse)
|
164 |
+
lat, lon = coo
|
165 |
+
|
166 |
+
G=download_graph(coo)
|
167 |
+
Gll=consolidate_intersections(G)
|
168 |
+
Gf=filter_elevations(Gll)
|
169 |
+
chemins=get_paths(Gf, lon, lat)
|
170 |
+
|
171 |
+
distances=[chemin[0] for chemin in chemins]
|
172 |
+
selected_points, distance = maxmin_dispersion(distances, p=par.nresults)
|
173 |
+
good_chemins=[chemin for chemin in chemins if chemin[0] in selected_points]
|
174 |
+
elevations={}
|
175 |
+
|
176 |
+
m=None
|
177 |
+
|
178 |
+
for ic, (distance, chemin) in enumerate(good_chemins):
|
179 |
+
dfg=ox.routing.route_to_gdf(Gf, chemin).to_crs("latlon")
|
180 |
+
dic_dfg[ic]=dfg
|
181 |
+
m=dfg.explore(m=m, color=cc.b_glasbey_category10[ic], line_width=2, tiles="Cartodb Positron")
|
182 |
+
elevations[ic]=get_elevation(dfg, Gll)
|
183 |
+
|
184 |
+
|
185 |
+
folium.Marker( location=[lat, lon], tooltip=par.adresse,
|
186 |
+
icon=folium.Icon(prefix="fa", icon="fa-person-running")).add_to(m)
|
187 |
+
|
188 |
+
carte.object=m
|
189 |
+
labels=[]
|
190 |
+
|
191 |
+
fig.data=[]
|
192 |
+
|
193 |
+
for idx, df in elevations.items():
|
194 |
+
label=str(idx+1)
|
195 |
+
labels.append(label)
|
196 |
+
fig.add_trace(go.Scatter(x=df.segcumsum, y=df.elevation, name=label,
|
197 |
+
mode='lines', line_color=cc.b_glasbey_category10[idx], line_shape='spline'))
|
198 |
+
|
199 |
+
fig.update_layout(xaxis_title="Distance", yaxis_title="Élévation", margin=dict(l=20, r=20, t=20, b=20),)
|
200 |
+
|
201 |
+
choixtrace.options=labels
|
202 |
+
carte.visible=True
|
203 |
+
filedownload.visible=True
|
204 |
+
elev.visible=True
|
205 |
+
choixtrace.visible=True
|
206 |
+
|
207 |
+
|
208 |
+
calcul.on_click(calculs)
|
209 |
+
|
210 |
+
|
211 |
+
app=pn.Column(pn.Row(pn.Column( pn.Param(par, width=300, name="Paramètres"), tuiles, calcul, choixtrace, filedownload),
|
212 |
+
elev),
|
213 |
+
carte)
|
214 |
+
app.servable()
|
uv.lock
ADDED
The diff for this file is too large to render.
See raw diff
|
|