Maidenhead locators
Maidenhead 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.
Version 1.0.0 · 4 functions · 0 crates
Functions
What use maidenhead.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use maidenhead_encode, maidenhead_decode from maidenhead (Quillo 0.6).
maidenhead_encode(lat: float, lon: float, chars: int) → stror an errorThe locator of a point with chars characters (2, 4, 6, 8 or 10): 4 is a "grid square" of 2° × 1°, 6 a subsquare of about 5 × 2.5 km. Fields upper case, subsquares lower case, as is usual.
maidenhead_decode(locator: str) → anyor an errorA locator's square: { lat, lon, south, west, north, east }, the centre and the corners in degrees. Upper or lower case alike.
maidenhead_distance(from: str, to: str) → anyor an errorFrom one locator to another, centre to centre: { km, miles, bearing }, the great-circle distance and the initial bearing in degrees from north (for pointing an antenna).
maidenhead_valid(locator: str) → boolWhether locator is a well-formed locator of 2 to 10 characters.
Get it
$ quillo install maidenhead # maidenhead.qwer into this folder$ quillo install maidenhead --example # and its example, maidenhead.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/maidenhead.qwer$ curl -O https://quillolang.org/plugins/files/maidenhead.qweThen, in your program, all of it or only what you call:
use maidenhead.qweruse maidenhead_encode, maidenhead_decode from maidenhead // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
Example
// A ham radio contest log: each contact's grid square from its position, and how far it was.//// quillo run maidenhead.qwe then open http://localhost:8080/loguse "maidenhead.qwer"// The ARRL's station W1AW in Newington, Connecticut, and a contact in Munich.let HOME = maidenhead_encode(41.714775, -72.727260, 6)?route GET /log { let munich = maidenhead_encode(48.1467, 11.6083, 6)? { home: HOME, munich: munich, munich_square: maidenhead_decode(munich)?, contact: maidenhead_distance(HOME, munich)?, precise: maidenhead_encode(41.714775, -72.727260, 10)?, valid: maidenhead_valid("FN31pr"), invalid: maidenhead_valid("ZZ99"), }}route GET /grid/:locator = maidenhead_decode(locator)?$ quillo run maidenhead.qwe$ curl 'localhost:8080/log'$ curl 'localhost:8080/grid/JN58'Source
//! Maidenhead grid locators, as radio amateurs write them ("JN58td", "FN31pr"): 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.//! version: 1.0.0//!//! [dependencies]use serde_json::{json, Value};const R_KM: f64 = 6371.0088;// Each pair of characters: (longitude step, latitude step, base).fn steps() -> [(f64, f64, u8); 5] { [(20.0, 10.0, 18), (2.0, 1.0, 10), (2.0 / 24.0, 1.0 / 24.0, 24), (2.0 / 240.0, 1.0 / 240.0, 10), (2.0 / 5760.0, 1.0 / 5760.0, 24)]}fn digit(base: u8, v: u8, upper: bool) -> char { match base { 10 => (b'0' + v) as char, _ => (if upper { b'A' } else { b'a' } + v) as char, }}/// The locator of a point with `chars` characters (2, 4, 6, 8 or 10): 4 is a "grid square" of 2° × 1°, 6 a subsquare of about 5 × 2.5 km. Fields upper case, subsquares lower case, as is usual.pub fn maidenhead_encode(lat: f64, lon: f64, chars: i64) -> Result<String, String> { if !(-90.0..=90.0).contains(&lat) || !(-180.0..=180.0).contains(&lon) { return Err(format!("({}, {}) is not a latitude and longitude", lat, lon)); } if ![2, 4, 6, 8, 10].contains(&chars) { return Err(format!("a locator has 2, 4, 6, 8 or 10 characters, not {}", chars)); } let mut x = (lon + 180.0).min(359.999_999_9); let mut y = (lat + 90.0).min(179.999_999_9); let mut out = String::new(); for (i, (sx, sy, base)) in steps().iter().take(chars as usize / 2).enumerate() { let dx = ((x / sx).floor() as i64).clamp(0, *base as i64 - 1) as u8; let dy = ((y / sy).floor() as i64).clamp(0, *base as i64 - 1) as u8; out.push(digit(*base, dx, i == 0)); out.push(digit(*base, dy, i == 0)); x -= dx as f64 * sx; y -= dy as f64 * sy; } Ok(out)}fn decode(loc: &str) -> Result<(f64, f64, f64, f64), String> { let s: Vec<char> = loc.trim().chars().collect(); if s.is_empty() || s.len() % 2 == 1 || s.len() > 10 { return Err(format!("`{}` is not a locator: 2, 4, 6, 8 or 10 characters", loc)); } let (mut lon, mut lat) = (-180.0, -90.0); let mut last = (20.0, 10.0); for (i, (sx, sy, base)) in steps().iter().take(s.len() / 2).enumerate() { let mut val = |c: char| -> Result<f64, String> { let v = if *base == 10 { c.to_digit(10).map(|d| d as u8) } else { let u = c.to_ascii_uppercase(); if u.is_ascii_uppercase() { Some(u as u8 - b'A') } else { None } }; match v { Some(v) if v < *base => Ok(v as f64), _ => Err(format!("`{}` is not a locator: `{}` is out of place", loc, c)), } }; lon += val(s[2 * i])? * sx; lat += val(s[2 * i + 1])? * sy; last = (*sx, *sy); } Ok((lat, lon, last.1, last.0))}/// A locator's square: `{ lat, lon, south, west, north, east }`, the centre and the corners in degrees. Upper or lower case alike.pub fn maidenhead_decode(locator: &str) -> Result<Value, String> { let (s, w, h, wd) = decode(locator)?; Ok(json!({ "lat": s + h / 2.0, "lon": w + wd / 2.0, "south": s, "west": w, "north": s + h, "east": w + wd, }))}/// From one locator to another, centre to centre: `{ km, miles, bearing }`, the great-circle distance and the initial bearing in degrees from north (for pointing an antenna).pub fn maidenhead_distance(from: &str, to: &str) -> Result<Value, String> { let (s1, w1, h1, d1) = decode(from)?; let (s2, w2, h2, d2) = decode(to)?; let (la1, lo1) = ((s1 + h1 / 2.0).to_radians(), (w1 + d1 / 2.0).to_radians()); let (la2, lo2) = ((s2 + h2 / 2.0).to_radians(), (w2 + d2 / 2.0).to_radians()); let dl = lo2 - lo1; let a = ((la2 - la1) / 2.0).sin().powi(2) + la1.cos() * la2.cos() * (dl / 2.0).sin().powi(2); let km = 2.0 * R_KM * a.sqrt().atan2((1.0 - a).sqrt()); let y = dl.sin() * la2.cos(); let x = la1.cos() * la2.sin() - la1.sin() * la2.cos() * dl.cos(); let bearing = (y.atan2(x).to_degrees() + 360.0) % 360.0; let r1 = |v: f64| (v * 10.0).round() / 10.0; Ok(json!({ "km": r1(km), "miles": r1(km / 1.609344), "bearing": r1(bearing) }))}/// Whether `locator` is a well-formed locator of 2 to 10 characters.pub fn maidenhead_valid(locator: &str) -> bool { decode(locator).is_ok()}Related
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 downloadsMap projections projections.qwer · 1.0.0Coordinates moved between map projections offline, by EPSG code or PROJ string: GPS degrees to Web Mercator, UTM, the British, Dutch and Swiss grids and some 6,000 others, for points and whole GeoJSON.8 views · 9 downloadsReverse geocoding reverse_geocode.qwer · 1.0.0The nearest town or city to a point, offline, from about 150,000 places with their region and country code.6 views · 7 downloadsSpatial index spatial_index.qwer · 1.0.0An R-tree of places in memory: the nearest ones to a point, all within a distance, those in a map's view, and markers clustered for it.6 views · 8 downloadsGPX tracks gpx.qwer · 1.0.0GPX tracks from watches and phones read: distance, climbing, duration, moving pace, per-kilometre splits, and the points for a map line.7 views · 8 downloadsH3 hexagons 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 downloadsThread 0 posts
On Maidenhead locators: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.