From 77685cde30d3e7b59e147acf9df50a1ecd87b5e3 Mon Sep 17 00:00:00 2001 From: James Fysh Date: Mon, 17 Apr 2017 05:52:21 +1000 Subject: [PATCH 01/17] Initial rust implementation working implementation (passes all validation, enc/dec, shorten/recover unit-tests); probably room for improvement wrt comments, layout. --- rust/Cargo.toml | 11 ++ rust/src/codearea.rs | 30 ++++ rust/src/consts.rs | 47 ++++++ rust/src/interface.rs | 298 +++++++++++++++++++++++++++++++++++++++ rust/src/lib.rs | 11 ++ rust/src/private.rs | 74 ++++++++++ rust/tests/all_test.rs | 88 ++++++++++++ rust/tests/csv_reader.rs | 34 +++++ 8 files changed, 593 insertions(+) create mode 100644 rust/Cargo.toml create mode 100644 rust/src/codearea.rs create mode 100644 rust/src/consts.rs create mode 100644 rust/src/interface.rs create mode 100644 rust/src/lib.rs create mode 100644 rust/src/private.rs create mode 100644 rust/tests/all_test.rs create mode 100644 rust/tests/csv_reader.rs diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 00000000..3782d19d --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "olc" +description = "Library for translating between GPS coordinates (WGS84) and Open Location Code" +version = "0.1.0" +authors = ["James Fysh "] +license = "MIT/Apache-2.0" +repository = "https://github.com/JamesFysh/olc-rs" +keywords = ["geography", "geospatial", "gis", "gps", "olc"] + +[dependencies] +geo = "^0.4.3" diff --git a/rust/src/codearea.rs b/rust/src/codearea.rs new file mode 100644 index 00000000..619d2b58 --- /dev/null +++ b/rust/src/codearea.rs @@ -0,0 +1,30 @@ +use geo::{Point}; + +pub struct CodeArea { + pub south: f64, + pub west: f64, + pub north: f64, + pub east: f64, + pub center: Point, + pub code_length: usize, +} + +impl CodeArea { + pub fn new(south: f64, west: f64, north: f64, east: f64, code_length: usize) -> CodeArea { + CodeArea { + south: south, west: west, north: north, east: east, + center: Point::new((west + east) / 2f64, (south + north) / 2f64), + code_length: code_length + } + } + + pub fn merge(self, other: CodeArea) -> CodeArea { + CodeArea::new( + self.south + other.south, + self.west + other.west, + self.north + other.north, + self.east + other.east, + self.code_length + other.code_length + ) + } +} \ No newline at end of file diff --git a/rust/src/consts.rs b/rust/src/consts.rs new file mode 100644 index 00000000..f329959c --- /dev/null +++ b/rust/src/consts.rs @@ -0,0 +1,47 @@ +// A separator used to break the code into two parts to aid memorability. +pub const SEPARATOR: char = '+'; + +// The number of characters to place before the separator. +pub const SEPARATOR_POSITION: usize = 8; + +// The character used to pad codes. +pub const PADDING_CHAR: char = '0'; +pub const PADDING_CHAR_STR: &'static str = "0"; + +// The character set used to encode the values. +pub const CODE_ALPHABET: [char; 20] = [ +'2', '3', '4', '5', '6', +'7', '8', '9', 'C', 'F', +'G', 'H', 'J', 'M', 'P', +'Q', 'R', 'V', 'W', 'X', +]; + +// The base to use to convert numbers to/from. +pub const ENCODING_BASE: f64 = 20f64; + +// The maximum value for latitude in degrees. +pub const LATITUDE_MAX: f64 = 90f64; + +// The maximum value for longitude in degrees. +pub const LONGITUDE_MAX: f64 = 180f64; + +// Maxiumum code length using lat/lng pair encoding. The area of such a +// code is approximately 13x13 meters (at the equator), and should be suitable +// for identifying buildings. This excludes prefix and separator characters. +pub const PAIR_CODE_LENGTH: usize = 10; + +// The resolution values in degrees for each position in the lat/lng pair +// encoding. These give the place value of each position, and therefore the +// dimensions of the resulting area. +pub const PAIR_RESOLUTIONS: [f64; 5] = [ + 20.0f64, 1.0f64, 0.05f64, 0.0025f64, 0.000125f64 +]; + +// Number of columns in the grid refinement method. +pub const GRID_COLUMNS: f64 = 4f64; + +// Number of rows in the grid refinement method. +pub const GRID_ROWS: f64 = 5f64; + +// Minimum length of a code that can be shortened. +pub const MIN_TRIMMABLE_CODE_LEN: usize = 6; \ No newline at end of file diff --git a/rust/src/interface.rs b/rust/src/interface.rs new file mode 100644 index 00000000..efd4f532 --- /dev/null +++ b/rust/src/interface.rs @@ -0,0 +1,298 @@ +use std::result::Result; +use std::ascii::AsciiExt; + +use geo::Point; + +use codearea::CodeArea; + +use consts::{ + SEPARATOR, SEPARATOR_POSITION, PADDING_CHAR, PADDING_CHAR_STR, CODE_ALPHABET, ENCODING_BASE, + LATITUDE_MAX, LONGITUDE_MAX, PAIR_CODE_LENGTH, PAIR_RESOLUTIONS, GRID_COLUMNS, GRID_ROWS, + MIN_TRIMMABLE_CODE_LEN, +}; + +use private::{ + code_value, normalize_longitude, clip_latitude, compute_latitude_precision, prefix_by_reference, + narrow_region, +}; + +/// Determines if a code is a valid Open Location Code. +pub fn is_valid(_code: &str) -> bool { + let mut code: String = _code.to_string(); + if code.len() < 3 { + // A code must have at-least a separater character + 1 lat/lng pair + return false; + } + + // Validate separator character + if code.find(SEPARATOR).is_none() { + // The code MUST contain a separator character + return false; + } + if code.find(SEPARATOR) != code.rfind(SEPARATOR) { + // .. And only one separator character + return false; + } + let spos = code.find(SEPARATOR).unwrap(); + if spos % 2 == 1 || spos > SEPARATOR_POSITION { + // The separator must be in a valid location + return false; + } + if code.len() - spos - 1 == 1 { + // There must be > 1 character after the separator + return false; + } + + // Validate padding + let padstart = code.find(PADDING_CHAR); + if padstart.is_some() { + let ppos = padstart.unwrap(); + if ppos == 0 || ppos % 2 == 1 { + // Padding must be "within" the string, starting at an even position + return false; + } + if code.len() > spos + 1 { + // If there is padding, the code must end with the separator char + return false; + } + let eppos = code.rfind(PADDING_CHAR).unwrap(); + if eppos - ppos % 2 == 1 { + // Must have even number of padding chars + return false; + } + // Extract the padding from the code (mutates code) + let padding: String = code.drain(ppos..eppos+1).collect(); + if padding.chars().any(|c| c != PADDING_CHAR) { + // Padding must be one, contiguous block of padding chars + return false; + } + } + + // Validate all characters are permissible + code.chars() + .map(|c| c.to_ascii_uppercase()) + .all(|c| c == SEPARATOR || CODE_ALPHABET.contains(&c)) +} + +/// Determines if a code is a valid short code. +/// +/// A short Open Location Code is a sequence created by removing four or more +/// digits from an Open Location Code. It must include a separator character. +pub fn is_short(_code: &str) -> bool { + is_valid(_code) && + _code.find(SEPARATOR).unwrap() < SEPARATOR_POSITION +} + +/// Determines if a code is a valid full Open Location Code. +/// +/// Not all possible combinations of Open Location Code characters decode to +/// valid latitude and longitude values. This checks that a code is valid +/// and also that the latitude and longitude values are legal. If the prefix +/// character is present, it must be the first character. If the separator +/// character is present, it must be after four characters. +pub fn is_full(_code: &str) -> bool { + is_valid(_code) && !is_short(_code) +} + +/// Encode a location into an Open Location Code. +/// +/// Produces a code of the specified length, or the default length if no +/// length is provided. +/// The length determines the accuracy of the code. The default length is +/// 10 characters, returning a code of approximately 13.5x13.5 meters. Longer +/// codes represent smaller areas, but lengths > 14 are sub-centimetre and so +/// 11 or 12 are probably the limit of useful codes. +pub fn encode(c: Point, code_length: usize) -> String { + let mut lat = clip_latitude(c.lat()); + let mut lng = normalize_longitude(c.lng()); + + // Latitude 90 needs to be adjusted to be just less, so the returned code + // can also be decoded. + if lat == LATITUDE_MAX { + lat = lat - compute_latitude_precision(code_length); + } + + lat += LATITUDE_MAX; + lng += LONGITUDE_MAX; + + let mut code = String::new(); + let mut digit = 0; + while digit < code_length { + narrow_region(digit, &mut lat, &mut lng); + + let lat_digit = lat as usize; + let lng_digit = lng as usize; + if digit < 10 { + code.push(CODE_ALPHABET[lat_digit]); + code.push(CODE_ALPHABET[lng_digit]); + digit += 2; + } + else { + code.push(CODE_ALPHABET[4 * lat_digit + lng_digit]); + digit += 1; + } + lat = lat - lat_digit as f64; + lng = lng - lng_digit as f64; + if digit == SEPARATOR_POSITION { + code.push(SEPARATOR); + } + } + if digit < SEPARATOR_POSITION { + code.push_str( + PADDING_CHAR_STR.repeat(SEPARATOR_POSITION - digit).as_str() + ); + code.push(SEPARATOR); + } + code +} + +/// Decodes an Open Location Code into the location coordinates. +/// +/// Returns a CodeArea object that includes the coordinates of the bounding +/// box - the lower left, center and upper right. +pub fn decode(_code: &str) -> Result { + if !is_full(_code) { + return Err(format!("Code must be a valid full code: {}", _code)); + } + let code = _code.to_string() + .replace(SEPARATOR, "") + .replace(PADDING_CHAR_STR, "") + .to_uppercase(); + + let mut lat = -LATITUDE_MAX; + let mut lng = -LONGITUDE_MAX; + let mut lat_res = ENCODING_BASE * ENCODING_BASE; + let mut lng_res = ENCODING_BASE * ENCODING_BASE; + + for (idx, chr) in code.chars().enumerate() { + if idx < PAIR_CODE_LENGTH { + if idx % 2 == 0 { + lat_res /= ENCODING_BASE; + lat += lat_res * code_value(chr) as f64; + } else { + lng_res /= ENCODING_BASE; + lng += lng_res * code_value(chr) as f64; + } + } else { + lat_res /= GRID_ROWS; + lng_res /= GRID_COLUMNS; + lat += lat_res * (code_value(chr) as f64 / GRID_COLUMNS).trunc(); + + lng += lng_res * (code_value(chr) as f64 % GRID_COLUMNS); + } + } + Ok(CodeArea::new(lat, lng, lat + lat_res, lng + lng_res, code.len())) +} + +/// Remove characters from the start of an OLC code. +/// +/// This uses a reference location to determine how many initial characters +/// can be removed from the OLC code. The number of characters that can be +/// removed depends on the distance between the code center and the reference +/// location. +/// The minimum number of characters that will be removed is four. If more +/// than four characters can be removed, the additional characters will be +/// replaced with the padding character. At most eight characters will be +/// removed. +/// The reference location must be within 50% of the maximum range. This +/// ensures that the shortened code will be able to be recovered using +/// slightly different locations. +/// +/// It returns either the original code, if the reference location was not +/// close enough, or the . +pub fn shorten(_code: &str, latitude: f64, longitude: f64) -> Result { + if !is_full(_code) { + return Ok(_code.to_string()); + } + if _code.find(PADDING_CHAR).is_some() { + return Err("Cannot shorten padded codes".to_owned()); + } + + let codearea: CodeArea = decode(_code).unwrap(); + if codearea.code_length < MIN_TRIMMABLE_CODE_LEN { + return Err(format!("Code length must be at least {}", MIN_TRIMMABLE_CODE_LEN)); + } + + // How close are the latitude and longitude to the code center. + let range = (codearea.center.lat() - clip_latitude(latitude)).abs().max( + (codearea.center.lng() - normalize_longitude(longitude)).abs() + ); + + for i in 0..PAIR_RESOLUTIONS.len() - 2 { + // Check if we're close enough to shorten. The range must be less than 1/2 + // the resolution to shorten at all, and we want to allow some safety, so + // use 0.3 instead of 0.5 as a multiplier. + let idx = PAIR_RESOLUTIONS.len() - 2 - i; + if range < (PAIR_RESOLUTIONS[idx] * 0.3f64) { + let mut code = _code.to_string(); + code.drain(..((idx + 1) * 2)); + return Ok(code); + } + } + Ok(_code.to_string()) +} + +/// Recover the nearest matching code to a specified location. +/// +/// Given a short Open Location Code of between four and seven characters, +/// this recovers the nearest matching full code to the specified location. +/// The number of characters that will be prepended to the short code, depends +/// on the length of the short code and whether it starts with the separator. +/// If it starts with the separator, four characters will be prepended. If it +/// does not, the characters that will be prepended to the short code, where S +/// is the supplied short code and R are the computed characters, are as +/// follows: +/// +/// * SSSS -> RRRR.RRSSSS +/// * SSSSS -> RRRR.RRSSSSS +/// * SSSSSS -> RRRR.SSSSSS +/// * SSSSSSS -> RRRR.SSSSSSS +/// +/// Note that short codes with an odd number of characters will have their +/// last character decoded using the grid refinement algorithm. +/// +/// It returns the nearest full Open Location Code to the reference location +/// that matches the [shortCode]. Note that the returned code may not have the +/// same computed characters as the reference location (provided by +/// [referenceLatitude] and [referenceLongitude]). This is because it returns +/// the nearest match, not necessarily the match within the same cell. If the +/// passed code was not a valid short code, but was a valid full code, it is +/// returned unchanged. +pub fn recover_nearest(_code: &str, ref_lat: f64, ref_lng: f64) -> Result { + if !is_short(_code) { + if is_full(_code) { + return Ok(_code.to_string()); + } else { + return Err(format!("Passed short code is not valid: {}", _code)); + } + } + + let ref_latitude = clip_latitude(ref_lat); + let ref_longitude = normalize_longitude(ref_lng); + + let prefix_len = SEPARATOR_POSITION - _code.find(SEPARATOR).unwrap(); + let mut code = prefix_by_reference(ref_lat, ref_lng, prefix_len); + code.push_str(_code); + + let code_area = decode(code.as_str()).unwrap(); + + let area_range = compute_latitude_precision(prefix_len); + let area_edge = area_range / 2f64; + + let mut latitude = code_area.center.lat(); + let mut longitude = code_area.center.lng(); + + let latitude_diff = latitude - ref_latitude; + if latitude_diff > area_edge { + latitude -= area_range; + } else if latitude_diff < -area_edge { + latitude += area_range; + } + let longitude_diff = longitude - ref_longitude; + if longitude_diff > area_edge { + longitude -= area_range; + } else if longitude_diff < -area_edge { + longitude += area_range; + } + Ok(encode(Point::new(longitude, latitude), code_area.code_length)) +} \ No newline at end of file diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 00000000..f4efe8ee --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,11 @@ +extern crate geo; + +mod consts; +mod private; + +mod codearea; +pub use codearea::CodeArea; + +mod interface; +pub use interface::{is_valid, is_short, is_full, encode, decode, shorten, recover_nearest}; + diff --git a/rust/src/private.rs b/rust/src/private.rs new file mode 100644 index 00000000..d449c47c --- /dev/null +++ b/rust/src/private.rs @@ -0,0 +1,74 @@ +use consts::{ + CODE_ALPHABET, ENCODING_BASE, LATITUDE_MAX, LONGITUDE_MAX, PAIR_CODE_LENGTH, GRID_ROWS, + GRID_COLUMNS, +}; + +use interface::encode; + +use geo::Point; + +pub fn code_value(chr: char) -> usize { + for (i, c) in CODE_ALPHABET.iter().enumerate() { + if chr == *c { + return i; + } + } + 0 +} + +pub fn normalize_longitude(value: f64) -> f64 { + let mut result: f64 = value; + while result >= LONGITUDE_MAX { + result = result - LONGITUDE_MAX * 2f64; + } + while result < -LONGITUDE_MAX{ + result = result + LONGITUDE_MAX * 2f64; + } + result +} + +pub fn clip_latitude(latitude_degrees: f64) -> f64 { + latitude_degrees.min(LATITUDE_MAX).max(-LATITUDE_MAX) +} + +pub fn compute_latitude_precision(code_length: usize) -> f64 { + if code_length <= PAIR_CODE_LENGTH { + return ENCODING_BASE.powf((code_length as f64 / -2f64 + 2f64).floor()) + } + ENCODING_BASE.powf(-3f64) / GRID_ROWS.powf(code_length as f64 - PAIR_CODE_LENGTH as f64) +} + +pub fn prefix_by_reference(lat: f64, lng: f64, code_length: usize) -> String { + let precision = compute_latitude_precision(code_length); + let rounded_lat = (lat / precision).floor() * precision; + let rounded_lng = (lng / precision).floor() * precision; + let mut code = encode( + Point::new(rounded_lng, rounded_lat), + PAIR_CODE_LENGTH + ); + code.drain(code_length..); + code +} + +pub fn near(value: f64) -> bool { + value.trunc() != (value + 0.0000000001f64).trunc() +} + +pub fn narrow_region(digit: usize, lat: &mut f64, lng: &mut f64) { + if digit == 0 { + *lat /= ENCODING_BASE; + *lng /= ENCODING_BASE; + } else if digit < 10 { + *lat *= ENCODING_BASE; + *lng *= ENCODING_BASE; + } else { + *lat *= GRID_ROWS; + *lng *= GRID_COLUMNS + } + if near(*lat) { + *lat = lat.round(); + } + if near(*lng) { + *lng = lng.round(); + } +} \ No newline at end of file diff --git a/rust/tests/all_test.rs b/rust/tests/all_test.rs new file mode 100644 index 00000000..fd791af8 --- /dev/null +++ b/rust/tests/all_test.rs @@ -0,0 +1,88 @@ +extern crate olc; +extern crate geo; + +use std::vec::Vec; + +use olc::{is_valid, is_short, is_full}; +use olc::{encode, decode}; +use olc::{shorten, recover_nearest}; + +use geo::Point; + +mod csv_reader; + +use csv_reader::CSVReader; + +/// CSVReader is written to swallow errors; as such, we might "pass" tests because we didn't +/// actually run any! Thus, we use 'tested' below to count # lines read and hence to assert that +/// > 0 tests were executed. +/// +/// We could probably take it a little further, and assert that tested was >= # tests in the file +/// (allowing tests to be added, but assuming # tests will never be reduced). + +#[test] +fn is_valid_test() { + let mut tested = 0; + for line in CSVReader::new("validityTests.csv") { + let cols: Vec<&str> = line.split(',').collect(); + let code = cols[0]; + let _valid = cols[1] == "true"; + let _short = cols[2] == "true"; + let _full = cols[3] == "true"; + + assert_eq!(is_valid(code), _valid, "valid for code: {}", code); + assert_eq!(is_short(code), _short, "short for code: {}", code); + assert_eq!(is_full(code), _full, "full for code: {}", code); + + tested += 1; + } + assert!(tested > 0); +} + +#[test] +fn encode_decode_test() { + let mut tested = 0; + for line in CSVReader::new("encodingTests.csv") { + let cols: Vec<&str> = line.split(',').collect(); + let code = cols[0]; + let lat = cols[1].parse::().unwrap(); + let lng = cols[2].parse::().unwrap(); + let latlo = cols[3].parse::().unwrap(); + let lnglo = cols[4].parse::().unwrap(); + let lathi = cols[5].parse::().unwrap(); + let lnghi = cols[6].parse::().unwrap(); + + let codearea = decode(code).unwrap(); + assert_eq!( + encode(Point::new(lng, lat), codearea.code_length), + code, + "encoding lat={},lng={}", lat, lng + ); + assert!((latlo - codearea.south).abs() < 0.001f64); + assert!((lathi - codearea.north).abs() < 0.001f64); + assert!((lnglo - codearea.west).abs() < 0.001f64); + assert!((lnghi - codearea.east).abs() < 0.001f64); + + tested += 1; + } + assert!(tested > 0); +} + +#[test] +fn shorten_recovery_test() { + let mut tested = 0; + for line in CSVReader::new("shortCodeTests.csv") { + let cols: Vec<&str> = line.split(',').collect(); + let full_code = cols[0]; + let lat = cols[1].parse::().unwrap(); + let lng = cols[2].parse::().unwrap(); + let short_code = cols[3]; + + assert_eq!(shorten(full_code, lat, lng).unwrap(), short_code, "shorten"); + assert_eq!(recover_nearest(short_code, lat, lng), Ok(full_code.to_string()), "recover"); + + tested += 1; + } + + assert!(tested > 0); +} diff --git a/rust/tests/csv_reader.rs b/rust/tests/csv_reader.rs new file mode 100644 index 00000000..5ea54699 --- /dev/null +++ b/rust/tests/csv_reader.rs @@ -0,0 +1,34 @@ +use std::env::current_dir; +use std::fs::File; +use std::io::{BufRead, BufReader, Lines}; + +pub struct CSVReader { + iter: Lines> +} + +impl CSVReader { + pub fn new(csv_name: &str) -> CSVReader { + // Assumes we're called from /rust + let project_root = current_dir().unwrap(); + let olc_root = project_root.parent().unwrap(); + let csv_path = olc_root.join("test_data").join(csv_name); + CSVReader { + iter: BufReader::new(File::open(csv_path).unwrap()).lines(), + } + } +} + +impl Iterator for CSVReader { + type Item = String; + + fn next(&mut self) -> Option { + // Iterate lines in the CSV file, dropping empty & comment lines + while let Some(Ok(s)) = self.iter.next() { + if s.is_empty() || s.starts_with("#") { + continue; + } + return Some(s); + } + None + } +} \ No newline at end of file From 52553ca481f516ea36b1329dd681f1fe18c4d2d7 Mon Sep 17 00:00:00 2001 From: James Fysh Date: Mon, 17 Apr 2017 05:55:38 +1000 Subject: [PATCH 02/17] Revise Cargo.toml Correct the 'repository' field, since I decided to implement this as part of Google' open-location-code repository. --- rust/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 3782d19d..082abd39 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -4,7 +4,7 @@ description = "Library for translating between GPS coordinates (WGS84) and Open version = "0.1.0" authors = ["James Fysh "] license = "MIT/Apache-2.0" -repository = "https://github.com/JamesFysh/olc-rs" +repository = "https://github.com/JamesFysh/open-location-code" keywords = ["geography", "geospatial", "gis", "gps", "olc"] [dependencies] From 33eeb9702ec616cfbad48cd64822bf1008454218 Mon Sep 17 00:00:00 2001 From: James Fysh Date: Mon, 17 Apr 2017 06:21:16 +1000 Subject: [PATCH 03/17] TravisCI Attempt to get a basic CI build going for rust... --- .travis.yml | 1 + run_tests.sh | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index 5643bc2a..685aa706 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ env: - TEST_DIR=js - TEST_DIR=go - TEST_DIR=ruby + - TEST_DIR=rust # Test script to run. This is called once for each TEST_DIR value above. script: ./run_tests.sh diff --git a/run_tests.sh b/run_tests.sh index 545eae46..0b5d90d4 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -32,4 +32,10 @@ if [ "$TEST_DIR" == "ruby" ]; then exit fi +if [ "$TEST_DIR" == "rust" ]; then + rustup update stable + cd rust && cargo test + exit +fi + echo "Unknown test directory: $TEST_DIR" From ad2dfed8e75f85945351d3dae866ca952ea320ba Mon Sep 17 00:00:00 2001 From: James Fysh Date: Mon, 17 Apr 2017 06:50:55 +1000 Subject: [PATCH 04/17] Install rust/cargo before using them! --- run_tests.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/run_tests.sh b/run_tests.sh index 0b5d90d4..38a050fd 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -33,6 +33,7 @@ if [ "$TEST_DIR" == "ruby" ]; then fi if [ "$TEST_DIR" == "rust" ]; then + curl https://sh.rustup.rs -sSf | sh -s -- -y rustup update stable cd rust && cargo test exit From 3f119464e32ca6f06e6d86759d103d94e1bd21b4 Mon Sep 17 00:00:00 2001 From: James Fysh Date: Mon, 17 Apr 2017 06:55:11 +1000 Subject: [PATCH 05/17] This time, for sure! --- run_tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run_tests.sh b/run_tests.sh index 38a050fd..ee0b5341 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -35,7 +35,7 @@ fi if [ "$TEST_DIR" == "rust" ]; then curl https://sh.rustup.rs -sSf | sh -s -- -y rustup update stable - cd rust && cargo test + cd rust && rustup run stable cargo test exit fi From 15831b4532bf1588ac2bea4deedb60896e2f1731 Mon Sep 17 00:00:00 2001 From: James Fysh Date: Mon, 17 Apr 2017 06:59:44 +1000 Subject: [PATCH 06/17] Ahh, the joys of working with remote CI --- run_tests.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/run_tests.sh b/run_tests.sh index ee0b5341..4fe3b193 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -34,6 +34,7 @@ fi if [ "$TEST_DIR" == "rust" ]; then curl https://sh.rustup.rs -sSf | sh -s -- -y + export PATH=$PATH:`pwd`/.cargo/bin rustup update stable cd rust && rustup run stable cargo test exit From ee42858e8212aebbaf616ca2f339b6d0ba0243f6 Mon Sep 17 00:00:00 2001 From: James Fysh Date: Mon, 17 Apr 2017 07:02:44 +1000 Subject: [PATCH 07/17] Stab in the dark #13 --- run_tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run_tests.sh b/run_tests.sh index 4fe3b193..699b01f2 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -34,7 +34,7 @@ fi if [ "$TEST_DIR" == "rust" ]; then curl https://sh.rustup.rs -sSf | sh -s -- -y - export PATH=$PATH:`pwd`/.cargo/bin + export PATH=$PATH:$HOME/.cargo/bin rustup update stable cd rust && rustup run stable cargo test exit From 8cd36b65b3f3fcf0c89a7243e96bcf2c57db348a Mon Sep 17 00:00:00 2001 From: James Fysh Date: Tue, 18 Apr 2017 06:08:08 +1000 Subject: [PATCH 08/17] Revise the public interface Take all lng/lat coordinates as Point, rather than as two f64 values. --- rust/src/interface.rs | 23 ++++++++++------------- rust/src/private.rs | 9 +++++---- rust/tests/all_test.rs | 8 ++++++-- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/rust/src/interface.rs b/rust/src/interface.rs index efd4f532..ce5842ab 100644 --- a/rust/src/interface.rs +++ b/rust/src/interface.rs @@ -102,9 +102,9 @@ pub fn is_full(_code: &str) -> bool { /// 10 characters, returning a code of approximately 13.5x13.5 meters. Longer /// codes represent smaller areas, but lengths > 14 are sub-centimetre and so /// 11 or 12 are probably the limit of useful codes. -pub fn encode(c: Point, code_length: usize) -> String { - let mut lat = clip_latitude(c.lat()); - let mut lng = normalize_longitude(c.lng()); +pub fn encode(pt: Point, code_length: usize) -> String { + let mut lat = clip_latitude(pt.lat()); + let mut lng = normalize_longitude(pt.lng()); // Latitude 90 needs to be adjusted to be just less, so the returned code // can also be decoded. @@ -200,7 +200,7 @@ pub fn decode(_code: &str) -> Result { /// /// It returns either the original code, if the reference location was not /// close enough, or the . -pub fn shorten(_code: &str, latitude: f64, longitude: f64) -> Result { +pub fn shorten(_code: &str, ref_pt: Point) -> Result { if !is_full(_code) { return Ok(_code.to_string()); } @@ -214,8 +214,8 @@ pub fn shorten(_code: &str, latitude: f64, longitude: f64) -> Result Result Result { +pub fn recover_nearest(_code: &str, ref_pt: Point) -> Result { if !is_short(_code) { if is_full(_code) { return Ok(_code.to_string()); @@ -267,11 +267,8 @@ pub fn recover_nearest(_code: &str, ref_lat: f64, ref_lng: f64) -> Result Result area_edge { latitude -= area_range; } else if latitude_diff < -area_edge { latitude += area_range; } - let longitude_diff = longitude - ref_longitude; + let longitude_diff = longitude - normalize_longitude(ref_pt.lng()); if longitude_diff > area_edge { longitude -= area_range; } else if longitude_diff < -area_edge { diff --git a/rust/src/private.rs b/rust/src/private.rs index d449c47c..7c30d9a3 100644 --- a/rust/src/private.rs +++ b/rust/src/private.rs @@ -38,12 +38,13 @@ pub fn compute_latitude_precision(code_length: usize) -> f64 { ENCODING_BASE.powf(-3f64) / GRID_ROWS.powf(code_length as f64 - PAIR_CODE_LENGTH as f64) } -pub fn prefix_by_reference(lat: f64, lng: f64, code_length: usize) -> String { +pub fn prefix_by_reference(pt: Point, code_length: usize) -> String { let precision = compute_latitude_precision(code_length); - let rounded_lat = (lat / precision).floor() * precision; - let rounded_lng = (lng / precision).floor() * precision; let mut code = encode( - Point::new(rounded_lng, rounded_lat), + Point::new( + (pt.lng() / precision).floor() * precision, + (pt.lat() / precision).floor() * precision + ), PAIR_CODE_LENGTH ); code.drain(code_length..); diff --git a/rust/tests/all_test.rs b/rust/tests/all_test.rs index fd791af8..9128f5d6 100644 --- a/rust/tests/all_test.rs +++ b/rust/tests/all_test.rs @@ -78,8 +78,12 @@ fn shorten_recovery_test() { let lng = cols[2].parse::().unwrap(); let short_code = cols[3]; - assert_eq!(shorten(full_code, lat, lng).unwrap(), short_code, "shorten"); - assert_eq!(recover_nearest(short_code, lat, lng), Ok(full_code.to_string()), "recover"); + assert_eq!(shorten(full_code, Point::new(lng, lat)).unwrap(), short_code, "shorten"); + assert_eq!( + recover_nearest(short_code, Point::new(lng, lat)), + Ok(full_code.to_string()), + "recover" + ); tested += 1; } From e25df3700db74286de6862174aa3fe94b3efc3e1 Mon Sep 17 00:00:00 2001 From: James Fysh Date: Tue, 18 Apr 2017 06:15:07 +1000 Subject: [PATCH 09/17] Ignore rust.iml when publishing --- rust/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 082abd39..25f0b504 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -6,6 +6,7 @@ authors = ["James Fysh "] license = "MIT/Apache-2.0" repository = "https://github.com/JamesFysh/open-location-code" keywords = ["geography", "geospatial", "gis", "gps", "olc"] +exclude = ["rust.iml"] [dependencies] geo = "^0.4.3" From 655ff46a7cff5305d72e53d6a7c8ce41b20ca3fc Mon Sep 17 00:00:00 2001 From: James Fysh Date: Mon, 5 Jun 2017 05:48:28 +1000 Subject: [PATCH 10/17] Incorporate PR feedback - Project name, licensing, repository location - Newlines - Code-style - Corrections to unit-tests - Safer approach to floating-point comparison - Use of constants rather than magic numbers --- .travis.yml | 5 +++++ rust/Cargo.toml | 6 +++--- rust/src/codearea.rs | 3 ++- rust/src/consts.rs | 3 ++- rust/src/interface.rs | 8 ++++---- rust/src/private.rs | 16 +++++++++++++--- rust/tests/all_test.rs | 17 +++++++++-------- rust/tests/csv_reader.rs | 3 ++- 8 files changed, 40 insertions(+), 21 deletions(-) diff --git a/.travis.yml b/.travis.yml index 685aa706..39cc1670 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,11 @@ node_js: # gulp: required for JS testing before_install: - npm install -g gulp + - yes | sudo add-apt-repository ppa:hansjorg/rust + - sudo apt-get update + +install: + sudo apt-get install rust-nightly # Define the list of directories to execute tests in. env: diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 25f0b504..fa7e1ffc 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "olc" +name = "open-location-code" description = "Library for translating between GPS coordinates (WGS84) and Open Location Code" version = "0.1.0" authors = ["James Fysh "] -license = "MIT/Apache-2.0" -repository = "https://github.com/JamesFysh/open-location-code" +license = "Apache-2.0" +repository = "https://github.com/google/open-location-code" keywords = ["geography", "geospatial", "gis", "gps", "olc"] exclude = ["rust.iml"] diff --git a/rust/src/codearea.rs b/rust/src/codearea.rs index 619d2b58..d1dc3bcc 100644 --- a/rust/src/codearea.rs +++ b/rust/src/codearea.rs @@ -27,4 +27,5 @@ impl CodeArea { self.code_length + other.code_length ) } -} \ No newline at end of file +} + diff --git a/rust/src/consts.rs b/rust/src/consts.rs index f329959c..e60699bf 100644 --- a/rust/src/consts.rs +++ b/rust/src/consts.rs @@ -44,4 +44,5 @@ pub const GRID_COLUMNS: f64 = 4f64; pub const GRID_ROWS: f64 = 5f64; // Minimum length of a code that can be shortened. -pub const MIN_TRIMMABLE_CODE_LEN: usize = 6; \ No newline at end of file +pub const MIN_TRIMMABLE_CODE_LEN: usize = 6; + diff --git a/rust/src/interface.rs b/rust/src/interface.rs index ce5842ab..61e910fa 100644 --- a/rust/src/interface.rs +++ b/rust/src/interface.rs @@ -108,7 +108,7 @@ pub fn encode(pt: Point, code_length: usize) -> String { // Latitude 90 needs to be adjusted to be just less, so the returned code // can also be decoded. - if lat == LATITUDE_MAX { + if lat > LATITUDE_MAX || (LATITUDE_MAX - lat) < 1e-10f64 { lat = lat - compute_latitude_precision(code_length); } @@ -126,8 +126,7 @@ pub fn encode(pt: Point, code_length: usize) -> String { code.push(CODE_ALPHABET[lat_digit]); code.push(CODE_ALPHABET[lng_digit]); digit += 2; - } - else { + } else { code.push(CODE_ALPHABET[4 * lat_digit + lng_digit]); digit += 1; } @@ -292,4 +291,5 @@ pub fn recover_nearest(_code: &str, ref_pt: Point) -> Result usize { return i; } } - 0 + // We assume this function is only called by other functions that have + // already ensured that the characters in the passed-in code are all valid + // and have all been "treated" (upper-cased, padding and '+' stripped) + // + // If that is the case, we will always return above. + unreachable!(); } pub fn normalize_longitude(value: f64) -> f64 { @@ -52,6 +57,11 @@ pub fn prefix_by_reference(pt: Point, code_length: usize) -> String { } pub fn near(value: f64) -> bool { + // Detects real values that are "very near" the next integer value + // returning true when this is the case. + // + // I'm not particularly happy with this function, but I am at a loss + // for another (better?) way to achieve the required behaviour. value.trunc() != (value + 0.0000000001f64).trunc() } @@ -59,7 +69,7 @@ pub fn narrow_region(digit: usize, lat: &mut f64, lng: &mut f64) { if digit == 0 { *lat /= ENCODING_BASE; *lng /= ENCODING_BASE; - } else if digit < 10 { + } else if digit < PAIR_CODE_LENGTH { *lat *= ENCODING_BASE; *lng *= ENCODING_BASE; } else { @@ -72,4 +82,4 @@ pub fn narrow_region(digit: usize, lat: &mut f64, lng: &mut f64) { if near(*lng) { *lng = lng.round(); } -} \ No newline at end of file +} diff --git a/rust/tests/all_test.rs b/rust/tests/all_test.rs index 9128f5d6..3479ef16 100644 --- a/rust/tests/all_test.rs +++ b/rust/tests/all_test.rs @@ -1,11 +1,11 @@ -extern crate olc; +extern crate open_location_code; extern crate geo; use std::vec::Vec; -use olc::{is_valid, is_short, is_full}; -use olc::{encode, decode}; -use olc::{shorten, recover_nearest}; +use open_location_code::{is_valid, is_short, is_full}; +use open_location_code::{encode, decode}; +use open_location_code::{shorten, recover_nearest}; use geo::Point; @@ -58,10 +58,10 @@ fn encode_decode_test() { code, "encoding lat={},lng={}", lat, lng ); - assert!((latlo - codearea.south).abs() < 0.001f64); - assert!((lathi - codearea.north).abs() < 0.001f64); - assert!((lnglo - codearea.west).abs() < 0.001f64); - assert!((lnghi - codearea.east).abs() < 0.001f64); + assert!((latlo - codearea.south).abs() < 1e-10f64); + assert!((lathi - codearea.north).abs() < 1e-10f64); + assert!((lnglo - codearea.west).abs() < 1e-10f64); + assert!((lnghi - codearea.east).abs() < 1e-10f64); tested += 1; } @@ -90,3 +90,4 @@ fn shorten_recovery_test() { assert!(tested > 0); } + diff --git a/rust/tests/csv_reader.rs b/rust/tests/csv_reader.rs index 5ea54699..1ffefeee 100644 --- a/rust/tests/csv_reader.rs +++ b/rust/tests/csv_reader.rs @@ -31,4 +31,5 @@ impl Iterator for CSVReader { } None } -} \ No newline at end of file +} + From 37b2c36c930d6993bc4da3c5540727108bd2db12 Mon Sep 17 00:00:00 2001 From: James Fysh Date: Mon, 5 Jun 2017 08:00:56 +1000 Subject: [PATCH 11/17] Fix travis unit-tests --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 39cc1670..25f01914 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ language: node_js node_js: - "0.10" +sudo: true # Install package dependencies. See # http://docs.travis-ci.com/user/installing-dependencies/ From 904c44b01d072188c7f2e0e59e86880e89d9ad63 Mon Sep 17 00:00:00 2001 From: James Fysh Date: Mon, 5 Jun 2017 08:07:24 +1000 Subject: [PATCH 12/17] Use a PPA that actually exists? --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 25f01914..a1aa4f0d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ sudo: true # gulp: required for JS testing before_install: - npm install -g gulp - - yes | sudo add-apt-repository ppa:hansjorg/rust + - yes | sudo add-apt-repository ppa:jonathonf/rustlang - sudo apt-get update install: From c0fa5de7e7407c5961ed02f843b8f81806f95e80 Mon Sep 17 00:00:00 2001 From: James Fysh Date: Wed, 21 Jun 2017 09:06:21 +1000 Subject: [PATCH 13/17] TravisCI, again Having a proper look at this; The rust-nightly package is only build for 14.04. Travis (by default) uses 12.04 (Precise), but has a 14.04 image which you can opt for. Hopefully this doesn't break anything else! --- .travis.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a1aa4f0d..f1cae15b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,17 @@ -sudo: false # Travis only supports one language per project, so we need to put something # here. node_js is as good as any other I suppose. language: node_js node_js: - "0.10" +# For rust, we need to install cargo, rustc. In order to do that, we'll use +# apt-get, which requires sudo. sudo: true +# The rust-nightly package is only built for 14.04 (Trusty) and 16.04 (Xenial), +# so we cannot use the default Travis ubuntu image (Precise) for these tests. +dist: trusty + # Install package dependencies. See # http://docs.travis-ci.com/user/installing-dependencies/ # gulp: required for JS testing From 2ab0872de76d0db07f6aa8ab60e9df765c835ce2 Mon Sep 17 00:00:00 2001 From: James Fysh Date: Wed, 21 Jun 2017 09:11:47 +1000 Subject: [PATCH 14/17] Helps if we specify actually-existing packages! The PPA doesn't have a rust-nightly. It does have a rustc package, and also a separate cargo package. Let's try those! --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f1cae15b..30bd676b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ before_install: - sudo apt-get update install: - sudo apt-get install rust-nightly + sudo apt-get -y install rustc cargo # Define the list of directories to execute tests in. env: From c6ecb227498a70570753f2b3af0b6fc1022f702b Mon Sep 17 00:00:00 2001 From: James Fysh Date: Wed, 21 Jun 2017 09:15:20 +1000 Subject: [PATCH 15/17] Dependencies Not sure why the choice here by apt-get is to fail, rather than pull in the missing dependent package. Oh well --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 30bd676b..54e74ff2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ before_install: - sudo apt-get update install: - sudo apt-get -y install rustc cargo + sudo apt-get -y install libstd-rust-dev rustc cargo # Define the list of directories to execute tests in. env: From 019f8b40078aea26fd2094088d688de0b56892c5 Mon Sep 17 00:00:00 2001 From: James Fysh Date: Wed, 21 Jun 2017 09:25:24 +1000 Subject: [PATCH 16/17] Further deps .. Not excited to be putting specific version numbers in. Starting to wish I'd stuck with the rustup approach; it's a little more predicatable! --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 54e74ff2..b6b6d8eb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ before_install: - sudo apt-get update install: - sudo apt-get -y install libstd-rust-dev rustc cargo + sudo apt-get -y install libstd-rust-1.16 libstd-rust-dev rustc cargo # Define the list of directories to execute tests in. env: From 5e8614bc4a5552f8492d941d37fc607f3ad9fe48 Mon Sep 17 00:00:00 2001 From: James Fysh Date: Wed, 21 Jun 2017 09:30:13 +1000 Subject: [PATCH 17/17] More deps? --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index b6b6d8eb..65693c3c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,8 @@ dist: trusty before_install: - npm install -g gulp - yes | sudo add-apt-repository ppa:jonathonf/rustlang + - yes | sudo add-apt-repository ppa:jonathonf/llvm + - yes | sudo add-apt-repository ppa:jonathonf/gcc - sudo apt-get update install: