Plus codes
Plus codes (Open Location Codes), the addresses Google Maps gives places without a street: from a latitude and longitude, back to their area, shortened near a town and recovered from the short form.
Version 1.0.0 · 5 functions · 0 crates
Functions
What use plus_codes.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use plus_code_encode, plus_code_decode from plus_codes (Quillo 0.6).
plus_code_encode(lat: float, lon: float, length: int) → stror an errorThe plus code of a point with length digits: 10 (about 14 × 14 m, the usual), 11 (3 × 3 m) up to 15; 2, 4, 6 or 8 for larger areas, padded with 0.
plus_code_decode(code: str) → anyor an errorThe area of a full plus code: { lat, lon, south, west, north, east, length }, its centre, its edges and its number of digits.
plus_code_shorten(code: str, lat: float, lon: float) → stror an errorA full code without the digits a nearby place makes plain: "9G8F+6X" for a code near (lat, lon), the centre of a town. The code is given back whole when the place is too far.
plus_code_recover(short_code: str, lat: float, lon: float) → stror an errorThe full code of a short one ("9G8F+6X") given a point near it, such as the town it was written with: the nearest code that ends so.
plus_code_kind(code: str) → strWhat code is: "full", "short" or "invalid".
Get it
$ quillo install plus_codes # plus_codes.qwer into this folder$ quillo install plus_codes --example # and its example, plus_codes.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/plus_codes.qwer$ curl -O https://quillolang.org/plugins/files/plus_codes.qweThen, in your program, all of it or only what you call:
use plus_codes.qweruse plus_code_encode, plus_code_decode from plus_codes // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
Example
// Deliveries to places without a street address: the driver gets a plus code, the customer can type a short one.//// quillo run plus_codes.qwe then open http://localhost:8080/codesuse "plus_codes.qwer"route GET /codes { // Google's office in Mountain View, and the reference codes of the Open Location Code tests. let office = plus_code_encode(37.4220041, -122.0862462, 10)? { office: office, office_precise: plus_code_encode(37.4220041, -122.0862462, 11)?, area: plus_code_decode("7FG49QCJ+2V")?, region: plus_code_encode(20.3700625, 2.7821875, 4)?, near: plus_code_shorten("9C3W9QCJ+2VX", 51.3701125, -1.217765625)?, further: plus_code_shorten("9C3W9QCJ+2VX", 51.3708675, -1.217765625)?, recovered: plus_code_recover("9QCJ+2VX", 51.3708675, -1.217765625)?, kinds: [plus_code_kind("9C3W9QCJ+2VX"), plus_code_kind("QCJ+2VX"), plus_code_kind("9C3W9QCJ"), plus_code_kind("CJ+2VX")], }}// /where?code=CJ+2VX&lat=51.37&lon=-1.21: a code from a form, short or full, as a map position.route GET /where ? { code: str, lat: float = 51.37, lon: float = -1.21 } { let full = plus_code_recover(code, lat, lon)? merge({ code: full }, plus_code_decode(full)?)}$ quillo run plus_codes.qwe$ curl 'localhost:8080/codes'Source
//! Plus codes (Open Location Codes), Google Maps' addresses for places without a street address: "8FVC9G8F+6X" from a latitude and longitude, back to its area, shortened near a town ("9G8F+6X Zurich") and recovered.//! version: 1.0.0//!//! [dependencies]use serde_json::{json, Value};const ALPHABET: &[u8; 20] = b"23456789CFGHJMPQRVWX";const SEP: char = '+';const SEP_POS: usize = 8;const PAD: char = '0';const PAIR_LEN: usize = 10;const MAX_DIGITS: usize = 15;const PAIR_PRECISION: i64 = 8000;const FINAL_LAT: i64 = 8000 * 3125; // 5^5 grid rowsconst FINAL_LNG: i64 = 8000 * 1024; // 4^5 grid columnsconst PAIR_RES: [f64; 5] = [20.0, 1.0, 0.05, 0.0025, 0.000125];fn index(c: char) -> Option<i64> { let u = c.to_ascii_uppercase() as u8; ALPHABET.iter().position(|&a| a == u).map(|i| i as i64)}fn lat_precision(len: usize) -> f64 { if len <= PAIR_LEN { 20f64.powi((len as i32) / -2 + 2) } else { 20f64.powi(-3) / 5f64.powi(len as i32 - PAIR_LEN as i32) }}fn encode(lat: f64, lng: f64, len: usize) -> String { let mut lat = lat.clamp(-90.0, 90.0); let mut lng = lng; while lng < -180.0 { lng += 360.0 } while lng >= 180.0 { lng -= 360.0 } if lat == 90.0 { lat -= lat_precision(len); } let mut lat_v = (((lat + 90.0) * FINAL_LAT as f64 * 1e6).round() / 1e6).floor() as i64; let mut lng_v = (((lng + 180.0) * FINAL_LNG as f64 * 1e6).round() / 1e6).floor() as i64; let mut rev: Vec<char> = Vec::new(); if len > PAIR_LEN { for _ in 0..5 { let d = (lat_v % 5) * 4 + (lng_v % 4); rev.push(ALPHABET[d as usize] as char); lat_v /= 5; lng_v /= 4; } } else { lat_v /= 3125; lng_v /= 1024; } for _ in 0..PAIR_LEN / 2 { rev.push(ALPHABET[(lng_v % 20) as usize] as char); rev.push(ALPHABET[(lat_v % 20) as usize] as char); lat_v /= 20; lng_v /= 20; } let digits: String = rev.into_iter().rev().collect(); let code = format!("{}{}{}", &digits[..SEP_POS], SEP, &digits[SEP_POS..]); if len >= SEP_POS { code[..len + 1].to_string() } else { format!("{}{}{}", &code[..len], PAD.to_string().repeat(SEP_POS - len), SEP) }}fn valid(code: &str) -> bool { let code = code.trim(); if code.is_empty() || !code.is_ascii() { return false; } let seps: Vec<usize> = code.match_indices(SEP).map(|(i, _)| i).collect(); if seps.len() != 1 || seps[0] > SEP_POS || seps[0] % 2 == 1 { return false; } let sep = seps[0]; if let Some(p) = code.find(PAD) { if p == 0 || sep < SEP_POS { return false; } let pads = &code[p..sep]; if pads.len() % 2 == 1 || pads.chars().any(|c| c != PAD) || code.len() > sep + 1 { return false; } if p % 2 == 1 { return false; } } if code.len() - sep - 1 == 1 { return false; } code.chars().filter(|&c| c != SEP && c != PAD).all(|c| index(c).is_some())}fn is_short(code: &str) -> bool { valid(code) && code.find(SEP).map(|i| i < SEP_POS).unwrap_or(false)}fn is_full(code: &str) -> bool { if !valid(code) || is_short(code) { return false; } let c: Vec<char> = code.chars().collect(); if index(c[0]).unwrap_or(99) * 20 >= 180 { return false; } if c.len() > 1 && index(c[1]).unwrap_or(99) * 20 >= 360 { return false; } true}struct Area { south: f64, west: f64, north: f64, east: f64, len: usize }fn decode(code: &str) -> Result<Area, String> { if !is_full(code) { return Err(format!("`{}` is not a full plus code", code)); } let clean: Vec<char> = code.trim().chars().filter(|&c| c != SEP && c != PAD).map(|c| c.to_ascii_uppercase()).collect(); let mut nlat = -90 * PAIR_PRECISION; let mut nlng = -180 * PAIR_PRECISION; let (mut glat, mut glng) = (0i64, 0i64); let digits = clean.len().min(PAIR_LEN); let mut pv: i64 = 160_000; let mut i = 0; while i < digits { nlat += index(clean[i]).unwrap() * pv; nlng += index(clean[i + 1]).unwrap() * pv; if i < digits - 2 { pv /= 20; } i += 2; } let mut lat_p = pv as f64 / PAIR_PRECISION as f64; let mut lng_p = pv as f64 / PAIR_PRECISION as f64; if clean.len() > PAIR_LEN { let (mut rowpv, mut colpv) = (625i64, 256i64); let digits = clean.len().min(MAX_DIGITS); for i in PAIR_LEN..digits { let d = index(clean[i]).unwrap(); glat += (d / 4) * rowpv; glng += (d % 4) * colpv; if i < digits - 1 { rowpv /= 5; colpv /= 4; } } lat_p = rowpv as f64 / FINAL_LAT as f64; lng_p = colpv as f64 / FINAL_LNG as f64; } let lat = nlat as f64 / PAIR_PRECISION as f64 + glat as f64 / FINAL_LAT as f64; let lng = nlng as f64 / PAIR_PRECISION as f64 + glng as f64 / FINAL_LNG as f64; let r = |v: f64| (v * 1e14).round() / 1e14; Ok(Area { south: r(lat), west: r(lng), north: r(lat + lat_p), east: r(lng + lng_p), len: clean.len().min(MAX_DIGITS) })}/// The plus code of a point with `length` digits: 10 (about 14 × 14 m, the usual), 11 (3 × 3 m) up to 15; 2, 4, 6 or 8 for larger areas, padded with 0.pub fn plus_code_encode(lat: f64, lon: f64, length: i64) -> Result<String, String> { if length < 2 || (length < PAIR_LEN as i64 && length % 2 == 1) || length > MAX_DIGITS as i64 { return Err(format!("a plus code has 2, 4, 6, 8 or 10 to 15 digits, not {}", length)); } if !lat.is_finite() || !lon.is_finite() { return Err("the latitude and longitude must be numbers".into()); } Ok(encode(lat, lon, length as usize))}/// The area of a full plus code: `{ lat, lon, south, west, north, east, length }`, its centre, its edges and its number of digits.pub fn plus_code_decode(code: &str) -> Result<Value, String> { let a = decode(code)?; let lat = ((a.south + a.north) / 2.0).min(90.0); let lon = ((a.west + a.east) / 2.0).min(180.0); Ok(json!({ "lat": (lat * 1e10).round() / 1e10, "lon": (lon * 1e10).round() / 1e10, "south": a.south, "west": a.west, "north": a.north, "east": a.east, "length": a.len }))}/// A full code without the digits a nearby place makes plain: `"9G8F+6X"` for a code near (`lat`, `lon`), the centre of a town. The code is given back whole when the place is too far.pub fn plus_code_shorten(code: &str, lat: f64, lon: f64) -> Result<String, String> { if !is_full(code) { return Err(format!("`{}` is not a full plus code", code)); } if code.contains(PAD) { return Err(format!("`{}` is padded and cannot be shortened", code)); } let a = decode(code)?; if a.len < 8 { return Err(format!("`{}` is too short to shorten", code)); } let up = code.trim().to_ascii_uppercase(); let range = (((a.south + a.north) / 2.0) - lat.clamp(-90.0, 90.0)).abs().max((((a.west + a.east) / 2.0) - lon).abs()); for i in (1..=3).rev() { if range < PAIR_RES[i] * 0.3 { return Ok(up[(i + 1) * 2..].to_string()); } } Ok(up)}/// The full code of a short one (`"9G8F+6X"`) given a point near it, such as the town it was written with: the nearest code that ends so.pub fn plus_code_recover(short_code: &str, lat: f64, lon: f64) -> Result<String, String> { let code = short_code.trim().to_ascii_uppercase(); if is_full(&code) { return Ok(code); } if !is_short(&code) { return Err(format!("`{}` is not a plus code", short_code)); } let lat = lat.clamp(-90.0, 90.0); let mut lon = lon; while lon < -180.0 { lon += 360.0 } while lon >= 180.0 { lon -= 360.0 } let pad = SEP_POS - code.find(SEP).unwrap(); let res = 20f64.powf(2.0 - pad as f64 / 2.0); let half = res / 2.0; let full = format!("{}{}", &encode(lat, lon, PAIR_LEN)[..pad], code); let a = decode(&full)?; let mut clat = (a.south + a.north) / 2.0; let mut clng = (a.west + a.east) / 2.0; if lat + half < clat && clat - res >= -90.0 { clat -= res; } else if lat - half > clat && clat + res <= 90.0 { clat += res; } if lon + half < clng { clng -= res; } else if lon - half > clng { clng += res; } Ok(encode(clat, clng, a.len))}/// What `code` is: `"full"`, `"short"` or `"invalid"`.pub fn plus_code_kind(code: &str) -> String { if is_full(code) { "full" } else if is_short(code) { "short" } else { "invalid" }.to_string()}Related
h3.qwer · 1.0.0H3 hexagon cells: the cell of a point at a resolution, its neighbours within k rings, its outline for a map, and distances between cells.7 views · 8 downloadsPolygons polygons.qwer · 1.0.0Points and areas from GeoJSON: whether a point is in a polygon, which zone holds it, and an area's size, perimeter, centre and bounding box.7 views · 8 downloadsCoordinates coordinates.qwer · 1.0.0Coordinates as people write them, such as 52°22'N 4°53'E or N 52° 22.2', read as one, written as degrees-minutes-seconds, and converted to and from UTM.7 views · 8 downloadsKML and KMZ kml.qwer · 1.0.0KML and KMZ files from Google Earth and My Maps read to GeoJSON, placemarks with their names, descriptions, folders and extended data, and GeoJSON written back as KML or KMZ.7 views · 8 downloadsMaidenhead locators maidenhead.qwer · 1.0.0Maidenhead grid locators as radio amateurs use them (FN31pr, JN58td): from a latitude and longitude at 2 to 10 characters, back to the square's centre and corners, and the distance and bearing between two.7 views · 8 downloadsMap clusters map_clusters.qwer · 1.0.0Map markers clustered by zoom as Supercluster does: thousands of points loaded once, then for each view and zoom the clusters with their counts and the single points, a cluster's points, and the zoom where it opens.7 views · 7 downloadsThread 0 posts
On Plus codes: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.