toolbox_rs/
polyline.rs

1//! Polyline encoding and decoding utilities
2//!
3//! This module provides functionality to encode and decode polylines using Google's
4//! polyline algorithm. See https://developers.google.com/maps/documentation/utilities/polylinealgorithm
5//!
6//! Reproduced from the above link to comply with the Apache License, Version 2.0
7//!
8//! Licensed under the Apache License, Version 2.0 (the "License");
9//! you may not use this file except in compliance with the License.
10//! You may obtain a copy of the License at
11//!
12//! http://www.apache.org/licenses/LICENSE-2.0
13//!
14//! Unless required by applicable law or agreed to in writing, software
15//! distributed under the License is distributed on an "AS IS" BASIS,
16//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17//! See the License for the specific language governing permissions and
18//! limitations under the License.
19
20/// Decodes an encoded path string into a sequence of coordinates
21///
22/// # Arguments
23/// * `encoded_path` - The encoded polyline string
24/// * `precision` - The precision factor (default: 5)
25///
26/// # Examples
27/// ```
28/// use toolbox_rs::polyline::decode;
29///
30/// let encoded = "_p~iF~ps|U_ulLnnqC_mqNvxq`@";
31/// let points = decode(encoded, 5);
32/// assert_eq!(points.len(), 3);
33/// assert!((points[0][0] - 38.5).abs() < 1e-10);
34/// ```
35pub fn decode(encoded_path: &str, precision: i32) -> Vec<[f64; 2]> {
36    let factor = 10f64.powi(precision);
37    let len = encoded_path.len();
38    let mut path = Vec::with_capacity(len / 2);
39    let mut index = 0;
40    let mut lat = 0;
41    let mut lng = 0;
42
43    while index < len {
44        let (result, new_index) = decode_unsigned(encoded_path.as_bytes(), index);
45        index = new_index;
46        lat += if result & 1 != 0 {
47            !(result >> 1)
48        } else {
49            result >> 1
50        };
51
52        let (result, new_index) = decode_unsigned(encoded_path.as_bytes(), index);
53        index = new_index;
54        lng += if result & 1 != 0 {
55            !(result >> 1)
56        } else {
57            result >> 1
58        };
59
60        path.push([lat as f64 / factor, lng as f64 / factor]);
61    }
62
63    path
64}
65
66/// Encodes an array of coordinates into a polyline string
67///
68/// # Arguments
69/// * `path` - Array of coordinate pairs
70/// * `precision` - The precision factor (default: 5)
71///
72/// # Examples
73/// ```
74/// use toolbox_rs::polyline::encode;
75///
76/// let path = vec![[38.5, -120.2], [40.7, -120.95], [43.252, -126.453]];
77/// let encoded = encode(&path, 5);
78/// assert_eq!(std::str::from_utf8(&encoded).unwrap(), "_p~iF~ps|U_ulLnnqC_mqNvxq`@");
79/// ```
80pub fn encode(path: &[[f64; 2]], precision: i32) -> Vec<u8> {
81    let factor = 10f64.powi(precision);
82    let transform = |point: &[f64; 2]| -> [i32; 2] {
83        [
84            (point[0] * factor).round() as i32,
85            (point[1] * factor).round() as i32,
86        ]
87    };
88
89    polyline_encode_line(path, transform)
90}
91
92#[inline(always)]
93fn decode_unsigned(encoded: &[u8], mut index: usize) -> (i32, usize) {
94    let mut result = 1;
95    let mut shift = 0;
96
97    while let Some(&byte) = encoded.get(index) {
98        let b = (byte as i32) - 63 - 1;
99        index += 1;
100        result += b << shift;
101        shift += 5;
102        if b < 0x1f {
103            break;
104        }
105    }
106
107    (result, index)
108}
109
110fn polyline_encode_line<F>(path: &[[f64; 2]], transform: F) -> Vec<u8>
111where
112    F: Fn(&[f64; 2]) -> [i32; 2],
113{
114    // guess a rough estimate of the capacity leading to less reallocations
115    let mut result = Vec::with_capacity(path.len() * 4);
116    let mut start = [0, 0];
117
118    for point in path {
119        let end = transform(point);
120        polyline_encode_signed(end[0] - start[0], &mut result);
121        polyline_encode_signed(end[1] - start[1], &mut result);
122        start = end;
123    }
124
125    result
126}
127
128fn polyline_encode_signed(value: i32, result: &mut Vec<u8>) {
129    polyline_encode_unsigned(if value < 0 { !(value << 1) } else { value << 1 }, result);
130}
131
132fn polyline_encode_unsigned(mut value: i32, result: &mut Vec<u8>) {
133    while value >= 0x20 {
134        result.push(((0x20 | (value & 0x1f)) + 63) as u8);
135        value >>= 5;
136    }
137    result.push((value + 63) as u8);
138}
139
140#[cfg(test)]
141mod tests {
142    use core::str;
143
144    use super::*;
145
146    // test data from Google's polyline algorithm documentation
147    const DEFAULT: [[f64; 2]; 3] = [[38.5, -120.2], [40.7, -120.95], [43.252, -126.453]];
148
149    const DEFAULT_ROUNDED: [[f64; 2]; 3] = [[39.0, -120.0], [41.0, -121.0], [43.0, -126.0]];
150
151    const SLASHES: [[f64; 2]; 3] = [[35.6, -82.55], [35.59985, -82.55015], [35.6, -82.55]];
152
153    const ROUNDING: [[f64; 2]; 2] = [[0.0, 0.000006], [0.0, 0.000002]];
154
155    const NEGATIVE: [[f64; 2]; 3] = [
156        [36.05322, -112.084004],
157        [36.053573, -112.083914],
158        [36.053845, -112.083965],
159    ];
160
161    #[test]
162    fn decode_empty() {
163        assert!(decode("", 5).is_empty());
164    }
165
166    #[test]
167    fn decode_default() {
168        let decoded = decode("_p~iF~ps|U_ulLnnqC_mqNvxq`@", 5);
169        for (i, point) in DEFAULT.iter().enumerate() {
170            assert!((decoded[i][0] - point[0]).abs() < 1e-5);
171            assert!((decoded[i][1] - point[1]).abs() < 1e-5);
172        }
173    }
174
175    #[test]
176    fn decode_custom_precision() {
177        let decoded = decode("_izlhA~rlgdF_{geC~ywl@_kwzCn`{nI", 6);
178        for (i, point) in DEFAULT.iter().enumerate() {
179            assert!((decoded[i][0] - point[0]).abs() < 1e-6);
180            assert!((decoded[i][1] - point[1]).abs() < 1e-6);
181        }
182    }
183
184    #[test]
185    fn decode_precision_zero() {
186        let decoded = decode("mAnFC@CH", 0);
187        for (i, point) in DEFAULT_ROUNDED.iter().enumerate() {
188            assert!((decoded[i][0] - point[0]).abs() < 1.0);
189            assert!((decoded[i][1] - point[1]).abs() < 1.0);
190        }
191    }
192
193    #[test]
194    fn roundtrip() {
195        let encoded = "gcneIpgxzRcDnBoBlEHzKjBbHlG`@`IkDxIiKhKoMaLwTwHeIqHuAyGXeB~Ew@fFjAtIzExF";
196        let decoded = decode(encoded, 5);
197        assert_eq!(str::from_utf8(&encode(&decoded, 5)).unwrap(), encoded);
198    }
199
200    #[test]
201    fn roundtrip_slashes() {
202        let encoded = encode(&SLASHES, 5);
203        let decoded = decode(str::from_utf8(&encoded).unwrap(), 5);
204        for (i, point) in SLASHES.iter().enumerate() {
205            assert!((decoded[i][0] - point[0]).abs() < 1e-5);
206            assert!((decoded[i][1] - point[1]).abs() < 1e-5);
207        }
208    }
209
210    #[test]
211    fn encode_empty() {
212        assert_eq!(str::from_utf8(&encode(&[], 5)).unwrap(), "");
213    }
214
215    #[test]
216    fn encode_default() {
217        assert_eq!(
218            str::from_utf8(&encode(&DEFAULT, 5)).unwrap(),
219            "_p~iF~ps|U_ulLnnqC_mqNvxq`@"
220        );
221    }
222
223    #[test]
224    fn encode_rounding() {
225        assert_eq!(str::from_utf8(&encode(&ROUNDING, 5)).unwrap(), "?A?@");
226    }
227
228    #[test]
229    fn encode_negative() {
230        assert_eq!(
231            str::from_utf8(&encode(&NEGATIVE, 5)).unwrap(),
232            "ss`{E~kbkTeAQw@J"
233        );
234    }
235
236    #[test]
237    fn encode_custom_precision() {
238        assert_eq!(
239            str::from_utf8(&encode(&DEFAULT, 6)).unwrap(),
240            "_izlhA~rlgdF_{geC~ywl@_kwzCn`{nI"
241        );
242    }
243
244    #[test]
245    fn encode_precision_zero() {
246        assert_eq!(str::from_utf8(&encode(&DEFAULT, 0)).unwrap(), "mAnFC@CH");
247    }
248
249    #[test]
250    fn encode_negative_values() {
251        let point = [[-107.3741825, 0.0]];
252        let encoded = encode(&point, 7);
253        let decoded = decode(str::from_utf8(&encoded).unwrap(), 7);
254        assert!(decoded[0][0] < 0.0);
255    }
256
257    #[test]
258    fn encode_decode() {
259        let points = vec![[38.5, -120.2], [40.7, -120.95], [43.252, -126.453]];
260        let encoded = encode(&points, 5);
261        assert_eq!(
262            str::from_utf8(&encoded).unwrap(),
263            "_p~iF~ps|U_ulLnnqC_mqNvxq`@"
264        );
265
266        let decoded = decode(str::from_utf8(&encoded).unwrap(), 5);
267        for (i, point) in points.iter().enumerate() {
268            assert!((decoded[i][0] - point[0]).abs() < 1e-10);
269            assert!((decoded[i][1] - point[1]).abs() < 1e-10);
270        }
271    }
272}