brouter_client/
lib.rs

1use lazy_regex::regex;
2use log::info;
3use reqwest::blocking::Client;
4use reqwest::Url;
5use std::io::BufReader;
6
7// See https://github.com/abrensch/brouter/blob/77977677db5fe78593c6a55afec6a251e69b3449/brouter-server/src/main/java/btools/server/request/ServerHandler.java#L17
8
9#[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}