open_location_code/
interface.rs1use geo::Point;
2
3use codearea::CodeArea;
4
5use consts::{
6 SEPARATOR, SEPARATOR_POSITION, PADDING_CHAR, PADDING_CHAR_STR, CODE_ALPHABET, ENCODING_BASE,
7 LATITUDE_MAX, LONGITUDE_MAX, PAIR_CODE_LENGTH, PAIR_RESOLUTIONS, GRID_COLUMNS, GRID_ROWS,
8 MIN_TRIMMABLE_CODE_LEN,
9};
10
11use private::{
12 code_value, normalize_longitude, clip_latitude, compute_latitude_precision, prefix_by_reference,
13 narrow_region,
14};
15
16pub fn is_valid(_code: &str) -> bool {
18 let mut code: String = _code.to_string();
19 if code.len() < 3 {
20 return false;
22 }
23
24 if code.find(SEPARATOR).is_none() {
26 return false;
28 }
29 if code.find(SEPARATOR) != code.rfind(SEPARATOR) {
30 return false;
32 }
33 let spos = code.find(SEPARATOR).unwrap();
34 if spos % 2 == 1 || spos > SEPARATOR_POSITION {
35 return false;
37 }
38 if code.len() - spos - 1 == 1 {
39 return false;
41 }
42
43 let padstart = code.find(PADDING_CHAR);
45 if padstart.is_some() {
46 let ppos = padstart.unwrap();
47 if ppos == 0 || ppos % 2 == 1 {
48 return false;
50 }
51 if code.len() > spos + 1 {
52 return false;
54 }
55 let eppos = code.rfind(PADDING_CHAR).unwrap();
56 if eppos - ppos % 2 == 1 {
57 return false;
59 }
60 let padding: String = code.drain(ppos..eppos+1).collect();
62 if padding.chars().any(|c| c != PADDING_CHAR) {
63 return false;
65 }
66 }
67
68 code.chars()
70 .map(|c| c.to_ascii_uppercase())
71 .all(|c| c == SEPARATOR || CODE_ALPHABET.contains(&c))
72}
73
74pub fn is_short(_code: &str) -> bool {
79 is_valid(_code) &&
80 _code.find(SEPARATOR).unwrap() < SEPARATOR_POSITION
81}
82
83pub fn is_full(_code: &str) -> bool {
91 is_valid(_code) && !is_short(_code)
92}
93
94pub fn encode(pt: Point<f64>, code_length: usize) -> String {
103 let mut lat = clip_latitude(pt.lat());
104 let mut lng = normalize_longitude(pt.lng());
105
106 if lat > LATITUDE_MAX || (LATITUDE_MAX - lat) < 1e-10f64 {
109 lat -= compute_latitude_precision(code_length);
110 }
111
112 lat += LATITUDE_MAX;
113 lng += LONGITUDE_MAX;
114
115 let mut code = String::with_capacity(code_length + 1);
116 let mut digit = 0;
117 while digit < code_length {
118 narrow_region(digit, &mut lat, &mut lng);
119
120 let lat_digit = lat as usize;
121 let lng_digit = lng as usize;
122 if digit < PAIR_CODE_LENGTH {
123 code.push(CODE_ALPHABET[lat_digit]);
124 code.push(CODE_ALPHABET[lng_digit]);
125 digit += 2;
126 } else {
127 code.push(CODE_ALPHABET[4 * lat_digit + lng_digit]);
128 digit += 1;
129 }
130 lat -= lat_digit as f64;
131 lng -= lng_digit as f64;
132 if digit == SEPARATOR_POSITION {
133 code.push(SEPARATOR);
134 }
135 }
136 if digit < SEPARATOR_POSITION {
137 code.push_str(
138 PADDING_CHAR_STR.repeat(SEPARATOR_POSITION - digit).as_str()
139 );
140 code.push(SEPARATOR);
141 }
142 code
143}
144
145pub fn decode(_code: &str) -> Result<CodeArea, String> {
150 if !is_full(_code) {
151 return Err(format!("Code must be a valid full code: {}", _code));
152 }
153 let code = _code.to_string()
154 .replace(SEPARATOR, "")
155 .replace(PADDING_CHAR_STR, "")
156 .to_uppercase();
157
158 let mut lat = -LATITUDE_MAX;
159 let mut lng = -LONGITUDE_MAX;
160 let mut lat_res = ENCODING_BASE * ENCODING_BASE;
161 let mut lng_res = ENCODING_BASE * ENCODING_BASE;
162
163 for (idx, chr) in code.chars().enumerate() {
164 if idx < PAIR_CODE_LENGTH {
165 if idx % 2 == 0 {
166 lat_res /= ENCODING_BASE;
167 lat += lat_res * code_value(chr) as f64;
168 } else {
169 lng_res /= ENCODING_BASE;
170 lng += lng_res * code_value(chr) as f64;
171 }
172 } else {
173 lat_res /= GRID_ROWS;
174 lng_res /= GRID_COLUMNS;
175 lat += lat_res * (code_value(chr) as f64 / GRID_COLUMNS).trunc();
176
177 lng += lng_res * (code_value(chr) as f64 % GRID_COLUMNS);
178 }
179 }
180 Ok(CodeArea::new(lat, lng, lat + lat_res, lng + lng_res, code.len()))
181}
182
183pub fn shorten(_code: &str, ref_pt: Point<f64>) -> Result<String, String> {
200 if !is_full(_code) {
201 return Ok(_code.to_string());
202 }
203 if _code.find(PADDING_CHAR).is_some() {
204 return Err("Cannot shorten padded codes".to_owned());
205 }
206
207 let codearea: CodeArea = decode(_code).unwrap();
208 if codearea.code_length < MIN_TRIMMABLE_CODE_LEN {
209 return Err(format!("Code length must be at least {}", MIN_TRIMMABLE_CODE_LEN));
210 }
211
212 let range = (codearea.center.lat() - clip_latitude(ref_pt.lat())).abs().max(
214 (codearea.center.lng() - normalize_longitude(ref_pt.lng())).abs()
215 );
216
217 for i in 0..PAIR_RESOLUTIONS.len() - 2 {
218 let idx = PAIR_RESOLUTIONS.len() - 2 - i;
222 if range < (PAIR_RESOLUTIONS[idx] * 0.3f64) {
223 let mut code = _code.to_string();
224 code.drain(..((idx + 1) * 2));
225 return Ok(code);
226 }
227 }
228 Ok(_code.to_string())
229}
230
231pub fn recover_nearest(_code: &str, ref_pt: Point<f64>) -> Result<String, String> {
258 if !is_short(_code) {
259 if is_full(_code) {
260 return Ok(_code.to_string());
261 } else {
262 return Err(format!("Passed short code is not valid: {}", _code));
263 }
264 }
265
266 let prefix_len = SEPARATOR_POSITION - _code.find(SEPARATOR).unwrap();
267 let mut code = prefix_by_reference(ref_pt, prefix_len);
268 code.push_str(_code);
269
270 let code_area = decode(code.as_str()).unwrap();
271
272 let resolution = compute_latitude_precision(prefix_len);
273 let half_res = resolution / 2f64;
274
275 let mut latitude = code_area.center.lat();
276 let mut longitude = code_area.center.lng();
277
278 let ref_lat = clip_latitude(ref_pt.lat());
279 let ref_lng = normalize_longitude(ref_pt.lng());
280 if ref_lat + half_res < latitude && latitude - resolution >= -LATITUDE_MAX {
281 latitude -= resolution;
282 } else if ref_lat - half_res > latitude && latitude + resolution <= LATITUDE_MAX {
283 latitude += resolution;
284 }
285 if ref_lng + half_res < longitude {
286 longitude -= resolution;
287 } else if ref_lng - half_res > longitude {
288 longitude += resolution;
289 }
290 Ok(encode(Point::new(longitude, latitude), code_area.code_length))
291}
292