1use lazy_regex::regex;
2use log::info;
3use reqwest::blocking::Client;
4use reqwest::Url;
5use std::io::BufReader;
6
7#[derive(Debug, Clone)]
10pub enum Nogo {
11 Point {
12 point: Point,
13 radius: f64,
14 weight: Option<f64>,
15 },
16 Line {
17 points: Vec<Point>,
18 weight: Option<f64>,
19 },
20 Polygon {
21 points: Vec<Point>,
22 weight: Option<f64>,
23 },
24}
25
26#[derive(Debug, Clone)]
27pub struct Point {
28 lat: f64,
29 lon: f64,
30}
31
32impl From<geo_types::Point> for Point {
33 fn from(p: geo_types::Point<f64>) -> Self {
34 Point {
35 lat: p.y(),
36 lon: p.x(),
37 }
38 }
39}
40
41impl From<Point> for geo_types::Point<f64> {
42 fn from(p: Point) -> Self {
43 geo_types::Point::new(p.lon, p.lat)
44 }
45}
46
47#[derive(Debug)]
48pub enum Error {
49 InvalidGpx(String),
50 Http(reqwest::Error),
51 MissingDataFile(String),
52}
53
54impl std::error::Error for Error {}
55
56impl std::fmt::Display for Error {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 match self {
59 Error::InvalidGpx(s) => write!(f, "Invalid GPX: {}", s),
60 Error::Http(e) => write!(f, "HTTP error: {}", e),
61 Error::MissingDataFile(s) => write!(f, "Missing data file: {}", s),
62 }
63 }
64}
65
66impl Point {
67 pub fn new(lat: f64, lon: f64) -> Self {
68 Point { lat, lon }
69 }
70
71 pub fn lat(&self) -> f64 {
72 self.lat
73 }
74
75 pub fn lon(&self) -> f64 {
76 self.lon
77 }
78}
79
80pub struct Brouter {
81 client: Client,
82 base_url: Url,
83}
84
85impl Default for Brouter {
86 fn default() -> Self {
87 Self::new("http://localhost:17777")
88 }
89}
90
91#[derive(Debug, Clone, Copy, Default)]
92pub enum TurnInstructionMode {
93 #[default]
94 None = 0,
95 AutoChoose = 1,
96 LocusStyle = 2,
97 OsmandStyle = 3,
98 CommentStyle = 4,
99 GpsiesStyle = 5,
100 OruxStyle = 6,
101 LocusOldStyle = 7,
102}
103
104impl Brouter {
105 pub fn new(base_url: &str) -> Self {
106 Brouter {
107 client: Client::new(),
108 base_url: Url::parse(base_url).unwrap(),
109 }
110 }
111
112 pub fn upload_profile(&self, profile: &str, data: Vec<u8>) -> Result<(), Error> {
113 let url = self
114 .base_url
115 .join("brouter/profile")
116 .unwrap()
117 .join(profile)
118 .unwrap();
119
120 let response = self
121 .client
122 .post(url)
123 .body(data)
124 .send()
125 .map_err(Error::Http)?;
126
127 response.error_for_status().map_err(Error::Http).map(|_| ())
128 }
129
130 pub fn broute(
131 &self,
132 points: &[Point],
133 nogos: &[Nogo],
134 profile: &str,
135 alternativeidx: Option<u8>,
136 timode: Option<TurnInstructionMode>,
137 name: Option<&str>,
138 export_waypoints: bool,
139 ) -> Result<gpx::Gpx, Error> {
140 let lon_lat_strings: Vec<String> = points
141 .iter()
142 .map(|p| format!("{},{}", p.lon(), p.lat()))
143 .collect();
144
145 info!("Planning route along {:?}", points);
146
147 let lonlats = lon_lat_strings.join("|");
148
149 let nogos_string: String = nogos
150 .iter()
151 .filter_map(|nogo| match nogo {
152 Nogo::Point {
153 point,
154 radius,
155 weight,
156 } => {
157 let mut v = vec![point.lon(), point.lat(), *radius];
158 if let Some(weight) = weight {
159 v.push(*weight);
160 }
161 Some(
162 v.iter()
163 .map(|f| f.to_string())
164 .collect::<Vec<_>>()
165 .join(","),
166 )
167 }
168 Nogo::Polygon { .. } => None,
169 Nogo::Line { .. } => None,
170 })
171 .collect::<Vec<_>>()
172 .join("|");
173
174 let polylines = nogos
175 .iter()
176 .filter_map(|nogo| match nogo {
177 Nogo::Point { .. } => None,
178 Nogo::Polygon { .. } => None,
179 Nogo::Line { points, weight } => {
180 let mut v = points
181 .iter()
182 .flat_map(|p| vec![p.lon(), p.lat()])
183 .collect::<Vec<_>>();
184 if let Some(weight) = weight {
185 v.push(*weight);
186 }
187 Some(
188 v.iter()
189 .map(|f| f.to_string())
190 .collect::<Vec<_>>()
191 .join(","),
192 )
193 }
194 })
195 .collect::<Vec<_>>()
196 .join("|");
197
198 let polygons = nogos
199 .iter()
200 .filter_map(|nogo| match nogo {
201 Nogo::Point { .. } => None,
202 Nogo::Line { .. } => None,
203 Nogo::Polygon { points, weight } => {
204 let mut v = points
205 .iter()
206 .flat_map(|p| vec![p.lon(), p.lat()])
207 .collect::<Vec<_>>();
208 if let Some(weight) = weight {
209 v.push(*weight);
210 }
211 Some(
212 v.iter()
213 .map(|f| f.to_string())
214 .collect::<Vec<_>>()
215 .join(","),
216 )
217 }
218 })
219 .collect::<Vec<_>>()
220 .join("|");
221
222 let mut url = self.base_url.join("brouter").unwrap();
223
224 url.query_pairs_mut()
225 .append_pair("lonlats", &lonlats)
226 .append_pair("profile", profile)
227 .append_pair("format", "gpx");
228
229 if let Some(alternativeidx) = alternativeidx {
230 assert!((0..=3).contains(&alternativeidx));
231
232 url.query_pairs_mut()
233 .append_pair("alternativeidx", alternativeidx.to_string().as_str());
234 }
235
236 if let Some(timode) = timode {
237 url.query_pairs_mut()
238 .append_pair("timode", (timode as i32).to_string().as_str());
239 }
240
241 if !polygons.is_empty() {
242 url.query_pairs_mut().append_pair("polygons", &polygons);
243 }
244
245 if !nogos_string.is_empty() {
246 url.query_pairs_mut().append_pair("nogos", &nogos_string);
247 }
248
249 if !polylines.is_empty() {
250 url.query_pairs_mut().append_pair("polylines", &polylines);
251 }
252
253 if export_waypoints {
254 url.query_pairs_mut().append_pair("exportWaypoints", "1");
255 }
256
257 if let Some(name) = name {
258 url.query_pairs_mut().append_pair("trackname", name);
259 }
260
261 let response = self
262 .client
263 .get(url)
264 .timeout(std::time::Duration::from_secs(3600))
265 .send()
266 .map_err(Error::Http)?
267 .error_for_status()
268 .map_err(Error::Http)?;
269
270 let text = response.bytes().map_err(Error::Http)?.to_vec();
271
272 if let Some(m) = regex!("datafile (.*) not found\n"B).captures(text.as_slice()) {
273 return Err(Error::MissingDataFile(
274 String::from_utf8_lossy(m.get(1).unwrap().as_bytes()).to_string(),
275 ));
276 }
277
278 let gpx: gpx::Gpx = gpx::read(BufReader::new(text.as_slice())).map_err(|_e| {
279 Error::InvalidGpx(String::from_utf8_lossy(text.as_slice()).to_string())
280 })?;
281
282 Ok(gpx)
283 }
284}