open_location_code/
interface.rs

1use 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
16/// Determines if a code is a valid Open Location Code.
17pub fn is_valid(_code: &str) -> bool {
18    let mut code: String = _code.to_string();
19    if code.len() < 3 {
20        // A code must have at-least a separator character + 1 lat/lng pair
21        return false;
22    }
23
24    // Validate separator character
25    if code.find(SEPARATOR).is_none() {
26        // The code MUST contain a separator character
27        return false;
28    }
29    if code.find(SEPARATOR) != code.rfind(SEPARATOR) {
30        // .. And only one separator character
31        return false;
32    }
33    let spos = code.find(SEPARATOR).unwrap();
34    if spos % 2 == 1 || spos > SEPARATOR_POSITION {
35        // The separator must be in a valid location
36        return false;
37    }
38    if code.len() - spos - 1 == 1 {
39        // There must be > 1 character after the separator
40        return false;
41    }
42
43    // Validate padding
44    let padstart = code.find(PADDING_CHAR);
45    if padstart.is_some() {
46        let ppos = padstart.unwrap();
47        if ppos == 0 || ppos % 2 == 1 {
48            // Padding must be "within" the string, starting at an even position
49            return false;
50        }
51        if code.len() > spos + 1 {
52            // If there is padding, the code must end with the separator char
53            return false;
54        }
55        let eppos = code.rfind(PADDING_CHAR).unwrap();
56        if eppos - ppos % 2 == 1 {
57            // Must have even number of padding chars
58            return false;
59        }
60        // Extract the padding from the code (mutates code)
61        let padding: String = code.drain(ppos..eppos+1).collect();
62        if padding.chars().any(|c| c != PADDING_CHAR) {
63            // Padding must be one, contiguous block of padding chars
64            return false;
65        }
66    }
67
68    // Validate all characters are permissible
69    code.chars()
70        .map(|c| c.to_ascii_uppercase())
71        .all(|c| c == SEPARATOR || CODE_ALPHABET.contains(&c))
72}
73
74/// Determines if a code is a valid short code.
75///
76/// A short Open Location Code is a sequence created by removing four or more
77/// digits from an Open Location Code. It must include a separator character.
78pub fn is_short(_code: &str) -> bool {
79    is_valid(_code) &&
80        _code.find(SEPARATOR).unwrap() < SEPARATOR_POSITION
81}
82
83/// Determines if a code is a valid full Open Location Code.
84///
85/// Not all possible combinations of Open Location Code characters decode to
86/// valid latitude and longitude values. This checks that a code is valid
87/// and also that the latitude and longitude values are legal. If the prefix
88/// character is present, it must be the first character. If the separator
89/// character is present, it must be after four characters.
90pub fn is_full(_code: &str) -> bool {
91    is_valid(_code) && !is_short(_code)
92}
93
94/// Encode a location into an Open Location Code.
95///
96/// Produces a code of the specified length, or the default length if no
97/// length is provided.
98/// The length determines the accuracy of the code. The default length is
99/// 10 characters, returning a code of approximately 13.5x13.5 meters. Longer
100/// codes represent smaller areas, but lengths > 14 are sub-centimetre and so
101/// 11 or 12 are probably the limit of useful codes.
102pub 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    // Latitude 90 needs to be adjusted to be just less, so the returned code
107    // can also be decoded.
108    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
145/// Decodes an Open Location Code into the location coordinates.
146///
147/// Returns a CodeArea object that includes the coordinates of the bounding
148/// box - the lower left, center and upper right.
149pub 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
183/// Remove characters from the start of an OLC code.
184///
185/// This uses a reference location to determine how many initial characters
186/// can be removed from the OLC code. The number of characters that can be
187/// removed depends on the distance between the code center and the reference
188/// location.
189/// The minimum number of characters that will be removed is four. If more
190/// than four characters can be removed, the additional characters will be
191/// replaced with the padding character. At most eight characters will be
192/// removed.
193/// The reference location must be within 50% of the maximum range. This
194/// ensures that the shortened code will be able to be recovered using
195/// slightly different locations.
196///
197/// It returns either the original code, if the reference location was not
198/// close enough, or the .
199pub 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    // How close are the latitude and longitude to the code center.
213    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        // Check if we're close enough to shorten. The range must be less than 1/2
219        // the resolution to shorten at all, and we want to allow some safety, so
220        // use 0.3 instead of 0.5 as a multiplier.
221        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
231/// Recover the nearest matching code to a specified location.
232///
233/// Given a short Open Location Code of between four and seven characters,
234/// this recovers the nearest matching full code to the specified location.
235/// The number of characters that will be prepended to the short code, depends
236/// on the length of the short code and whether it starts with the separator.
237/// If it starts with the separator, four characters will be prepended. If it
238/// does not, the characters that will be prepended to the short code, where S
239/// is the supplied short code and R are the computed characters, are as
240/// follows:
241///
242/// * SSSS    -> RRRR.RRSSSS
243/// * SSSSS   -> RRRR.RRSSSSS
244/// * SSSSSS  -> RRRR.SSSSSS
245/// * SSSSSSS -> RRRR.SSSSSSS
246///
247/// Note that short codes with an odd number of characters will have their
248/// last character decoded using the grid refinement algorithm.
249///
250/// It returns the nearest full Open Location Code to the reference location
251/// that matches the [shortCode]. Note that the returned code may not have the
252/// same computed characters as the reference location (provided by
253/// [referenceLatitude] and [referenceLongitude]). This is because it returns
254/// the nearest match, not necessarily the match within the same cell. If the
255/// passed code was not a valid short code, but was a valid full code, it is
256/// returned unchanged.
257pub 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