Coordinates
Coordinates 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.
Version 1.0.0 · 4 functions · 1 crate
Functions
What use coordinates.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use coords_parse, coords_to_dms from coordinates (Quillo 0.6).
coords_parse(text: str) → anyor an errorReads a coordinate pair as people write it: 52°22'12"N 4°53'24"E, 52 22 12 N, 4 53 24 E, N52°22.2' E4°53.4', S 33.92 E 18.42, 52.37, 4.89, -33.92 18.42. Gives { lat, lon } in decimal degrees (south and west negative); the halves may come in either order when they have N/S/E/W letters. O for Ost/Oeste reads as east.
coords_to_dms(lat: float, lon: float, decimals: int) → stror an errorA point in degrees, minutes and seconds: 52°22'12.0"N 4°53'24.0"E, seconds with decimals places (0 to 4).
coords_to_utm(lat: float, lon: float) → anyor an errorA point on the UTM grid (WGS 84): { zone, letter, easting, northing, text }, metres rounded to 1, text as 31U 628514 5803803. UTM covers latitudes -80 to 84.
coords_from_utm(text: str) → anyor an errorA UTM grid reference (31U 628514 5803803, or 31 U 628514 5803803) back to { lat, lon }.
Get it
$ quillo install coordinates # coordinates.qwer into this folder$ quillo install coordinates --example # and its example, coordinates.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/coordinates.qwer$ curl -O https://quillolang.org/plugins/files/coordinates.qweThen, in your program, all of it or only what you call:
use coordinates.qweruse coords_parse, coords_to_dms from coordinates // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
utm = "0.1"Example
// A "where is it" field that takes coordinates however people paste them, and shows them back// in degrees-minutes-seconds and on the UTM grid (for search and rescue, surveying, hiking maps).//// quillo run coordinates.qwe then open http://localhost:8080/where?q=52°22'12"N%204°53'24"Euse "coordinates.qwer"route GET /where ? { q: str } { let p = coords_parse(q)? { lat: p.lat, lon: p.lon, dms: coords_to_dms(p.lat, p.lon, 1)?, utm: coords_to_utm(p.lat, p.lon)?.text }}route GET /forms = ["52°22'12\"N 4°53'24\"E", "N 52° 22.2' E 4° 53.4'", "52.37, 4.89", "4°53'24\"E 52°22'12\"N", "S 33.9249 E 18.4241", "-33.9249 18.4241"] | map((q) => { let p = coords_parse(q)?; `${p.lat}, ${p.lon}` })route GET /utm = { cape_town: coords_to_utm(-33.92487, 18.42406)?, back: coords_from_utm("34H 261878 6243186")?, bad: coords_parse("52 N 4 N").error }$ quillo run coordinates.qwe$ curl 'localhost:8080/where?q=52%C2%B022'12%22N%204%C2%B053'24%22E'$ curl 'localhost:8080/forms'$ curl 'localhost:8080/utm'Source
//! Coordinates in the forms people and maps write them: "52°22'12"N 4°53'24"E", "N 52° 22.2' E 4° 53.4'" and "52.37, 4.89" read as one, written back as degrees-minutes-seconds, and converted to and from UTM grid references.//! version: 1.0.0//!//! [dependencies]//! utm = "0.1"use serde_json::{json, Value};enum Tok { Num(f64), Hemi(char),}fn tokens(text: &str) -> Result<Vec<Tok>, String> { let mut out = Vec::new(); let cs: Vec<char> = text.chars().collect(); let mut i = 0; while i < cs.len() { let c = cs[i]; if c.is_ascii_digit() || ((c == '-' || c == '+' || c == '.') && cs.get(i + 1).map(|n| n.is_ascii_digit() || *n == '.').unwrap_or(false)) { let start = i; i += 1; while i < cs.len() && (cs[i].is_ascii_digit() || cs[i] == '.') { i += 1; } let s: String = cs[start..i].iter().collect(); out.push(Tok::Num(s.parse::<f64>().map_err(|_| format!("`{}` is not a number", s))?)); continue; } match c.to_ascii_uppercase() { 'N' | 'S' | 'E' | 'W' | 'O' => out.push(Tok::Hemi(if c.to_ascii_uppercase() == 'O' { 'E' } else { c.to_ascii_uppercase() })), _ if c.is_alphabetic() => return Err(format!("`{}` is not part of a coordinate", c)), _ => {} } i += 1; } Ok(out)}fn value(nums: &[f64], hemi: Option<char>) -> Result<f64, String> { if nums.is_empty() || nums.len() > 3 { return Err("each half is degrees, with minutes and seconds if any".into()); } if nums[1..].iter().any(|x| *x < 0.0 || *x >= 60.0) { return Err("minutes and seconds are 0 to 60".into()); } let neg = nums[0] < 0.0; let mut v = nums[0].abs() + nums.get(1).unwrap_or(&0.0) / 60.0 + nums.get(2).unwrap_or(&0.0) / 3600.0; if neg || matches!(hemi, Some('S') | Some('W')) { v = -v; } Ok(v)}/// Reads a coordinate pair as people write it: `52°22'12"N 4°53'24"E`, `52 22 12 N, 4 53 24 E`, `N52°22.2' E4°53.4'`, `S 33.92 E 18.42`, `52.37, 4.89`, `-33.92 18.42`./// Gives `{ lat, lon }` in decimal degrees (south and west negative); the halves may come in either order when they have N/S/E/W letters. O for Ost/Oeste reads as east.pub fn coords_parse(text: &str) -> Result<Value, String> { if text.len() > 200 { return Err("a coordinate is at most 200 characters".into()); } let toks = tokens(text)?; let has_hemi = toks.iter().any(|t| matches!(t, Tok::Hemi(_))); let mut halves: Vec<(Vec<f64>, Option<char>)> = Vec::new(); if has_hemi { let prefix = matches!(toks.first(), Some(Tok::Hemi(_))); let mut cur: (Vec<f64>, Option<char>) = (vec![], None); for t in toks { match t { Tok::Num(n) => cur.0.push(n), Tok::Hemi(h) if prefix => { if cur.1.is_some() || !cur.0.is_empty() { halves.push(std::mem::take(&mut cur)); } cur.1 = Some(h); } Tok::Hemi(h) => { cur.1 = Some(h); halves.push(std::mem::take(&mut cur)); } } } if !cur.0.is_empty() { halves.push(cur); } } else { let nums: Vec<f64> = toks.into_iter().filter_map(|t| if let Tok::Num(n) = t { Some(n) } else { None }).collect(); if nums.len() % 2 != 0 || nums.is_empty() || nums.len() > 6 { return Err(format!("cannot tell the two halves apart in `{}`", text)); } let k = nums.len() / 2; halves.push((nums[..k].to_vec(), None)); halves.push((nums[k..].to_vec(), None)); } if halves.len() != 2 { return Err(format!("`{}` does not have two halves (latitude and longitude)", text)); } let lat_first = !matches!(halves[0].1, Some('E') | Some('W')) && !matches!(halves[1].1, Some('N') | Some('S')); let (a, b) = if lat_first { (&halves[0], &halves[1]) } else { (&halves[1], &halves[0]) }; if matches!(a.1, Some('E') | Some('W')) || matches!(b.1, Some('N') | Some('S')) { return Err("both halves are latitudes, or both longitudes".into()); } let (lat, lon) = (value(&a.0, a.1)?, value(&b.0, b.1)?); if !(-90.0..=90.0).contains(&lat) || !(-180.0..=180.0).contains(&lon) { return Err(format!("{}, {} is not a place", lat, lon)); } let r = |x: f64| (x * 1e7).round() / 1e7; Ok(json!({ "lat": r(lat), "lon": r(lon) }))}fn dms(v: f64, pos: char, neg: char, decimals: usize) -> String { let h = if v < 0.0 { neg } else { pos }; let a = v.abs(); let mut d = a.trunc(); let mut m = ((a - d) * 60.0).trunc(); let mut s = (a - d - m / 60.0) * 3600.0; let scale = 10f64.powi(decimals as i32); s = (s * scale).round() / scale; if s >= 60.0 { s -= 60.0; m += 1.0; } if m >= 60.0 { m -= 60.0; d += 1.0; } format!("{}°{:02}'{:0w$.p$}\"{}", d as i64, m as i64, s, h, w = if decimals > 0 { decimals + 3 } else { 2 }, p = decimals)}/// A point in degrees, minutes and seconds: `52°22'12.0"N 4°53'24.0"E`, seconds with `decimals` places (0 to 4).pub fn coords_to_dms(lat: f64, lon: f64, decimals: i64) -> Result<String, String> { if !(-90.0..=90.0).contains(&lat) || !(-180.0..=180.0).contains(&lon) { return Err(format!("{}, {} is not a place", lat, lon)); } let d = decimals.clamp(0, 4) as usize; Ok(format!("{} {}", dms(lat, 'N', 'S', d), dms(lon, 'E', 'W', d)))}/// A point on the UTM grid (WGS 84): `{ zone, letter, easting, northing, text }`, metres rounded to 1, `text` as `31U 628514 5803803`. UTM covers latitudes -80 to 84.pub fn coords_to_utm(lat: f64, lon: f64) -> Result<Value, String> { if !(-80.0..=84.0).contains(&lat) || !(-180.0..=180.0).contains(&lon) { return Err(format!("{}, {} is outside UTM (latitudes -80 to 84)", lat, lon)); } let zone = utm::lat_lon_to_zone_number(lat, lon); let letter = utm::lat_to_zone_letter(lat).ok_or("no UTM band for this latitude")?; let (mut n, e, _) = utm::to_utm_wgs84(lat, lon, zone); if n < 0.0 { n += 10_000_000.0; } let (e, n) = (e.round() as i64, n.round() as i64); Ok(json!({ "zone": zone, "letter": letter.to_string(), "easting": e, "northing": n, "text": format!("{}{} {} {}", zone, letter, e, n) }))}/// A UTM grid reference (`31U 628514 5803803`, or `31 U 628514 5803803`) back to `{ lat, lon }`.pub fn coords_from_utm(text: &str) -> Result<Value, String> { let t = text.trim().replace(',', " "); let parts: Vec<&str> = t.split_whitespace().collect(); let (zone_part, rest) = match parts.as_slice() { [z, l, e, n] if l.len() == 1 => (format!("{}{}", z, l), vec![*e, *n]), [zl, e, n] => (zl.to_string(), vec![*e, *n]), _ => return Err(format!("`{}` is not like 31U 628514 5803803", text)), }; let letter = zone_part.chars().last().filter(|c| c.is_ascii_alphabetic()).ok_or("the zone needs its band letter, as in 31U")?.to_ascii_uppercase(); let zone: u8 = zone_part[..zone_part.len() - 1].parse().map_err(|_| format!("`{}` is not a UTM zone", zone_part))?; let e: f64 = rest[0].parse().map_err(|_| format!("`{}` is not an easting", rest[0]))?; let n: f64 = rest[1].parse().map_err(|_| format!("`{}` is not a northing", rest[1]))?; let (lat, lon) = utm::wsg84_utm_to_lat_lon(e, n, zone, letter).map_err(|e| format!("{:?}", e))?; let r = |x: f64| (x * 1e6).round() / 1e6; Ok(json!({ "lat": r(lat), "lon": r(lon) }))}Related
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 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 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 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 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 Coordinates: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.