Places

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

#geo#maps#gps#location

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 error

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.

maidenhead_decode(locator: str) → anyor an error

A 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 error

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).

maidenhead_valid(locator: str) → bool

Whether 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.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/maidenhead.qwer$ curl -O https://quillolang.org/plugins/files/maidenhead.qwe

Then, in your program, all of it or only what you call:

main.qwe
use maidenhead.qweruse maidenhead_encode, maidenhead_decode from maidenhead      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]

Example

maidenhead.qwe
// 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.qwer
//! 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()}
Thread
78Useful? Like it, or add to its thread below.

Thread 0 posts

On Maidenhead locators: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.