// A permissive front door: one string in, one `Location` out. The result keeps
// the notation, the representative point, the region, the resolution and any
// explicit uncertainty, so callers can see what a conversion would lose.
///|
/// The notation a coordinate string was written in.
pub(all) enum LocationFormat {
GeoUri
PlusCode
Maidenhead
GeoHash
Decimal
Dms
} derive(Eq, Debug)
///|
pub impl Show for LocationFormat with fn to_string(self : LocationFormat) -> String {
match self {
GeoUri => "geo-uri"
PlusCode => "plus-code"
Maidenhead => "maidenhead"
GeoHash => "geohash"
Decimal => "decimal"
Dms => "dms"
}
}
///|
/// Recognise one coordinate written as a geo URI, Plus Code, Maidenhead
/// locator, geohash, decimal pair or DMS pair.
pub fn parse_location(input : String) -> Location? {
let s = trim_ascii(input)
if s == "" {
return None
}
if s.length() >= 4 && ascii_lower(s[0:4].to_owned()) == "geo:" {
match geouri_parse(s) {
Some(uri) => {
match uri.crs {
Some(crs) if ascii_lower(crs) != "wgs84" => return None
_ => ()
}
return point_location(
GeoUri,
s,
uri.point,
uri.altitude,
uri.crs,
uri.uncertainty,
)
}
None => return None
}
}
if s.contains("+") {
match pluscode_bounds(s) {
Some(b) => return cell_location(PlusCode, s, b)
None => ()
}
}
let fields = split_whitespace(s)
let comma = split_text(s, ',')
if comma.length() == 2 {
let lat = parse_decimal(comma[0])
let lng = parse_decimal(comma[1])
if lat is Some(a) && lng is Some(b) {
match LatLng::new(a, b) {
Some(p) => return point_location(Decimal, s, p, None, None, None)
None => ()
}
}
let lat_dms = parse_dms(comma[0], true)
let lng_dms = parse_dms(comma[1], false)
if lat_dms is Some(a) && lng_dms is Some(b) {
match LatLng::new(a, b) {
Some(p) => return point_location(Dms, s, p, None, None, None)
None => ()
}
}
}
if fields.length() == 2 {
let lat = parse_decimal(fields[0])
let lng = parse_decimal(fields[1])
if lat is Some(a) && lng is Some(b) {
match LatLng::new(a, b) {
Some(p) => return point_location(Decimal, s, p, None, None, None)
None => ()
}
}
let lat_dms = parse_dms(fields[0], true)
let lng_dms = parse_dms(fields[1], false)
if lat_dms is Some(a) && lng_dms is Some(b) {
match LatLng::new(a, b) {
Some(p) => return point_location(Dms, s, p, None, None, None)
None => ()
}
}
}
match maidenhead_bounds(s) {
Some(b) => return cell_location(Maidenhead, s, b)
None => ()
}
match geohash_bounds(s) {
Some(b) => return cell_location(GeoHash, s, b)
None => ()
}
None
}
///|
/// Scan free text for up to `max_results` location codes. Tokens are split on
/// whitespace and common surrounding punctuation is removed before each token
/// is offered to `parse_location`. This is deliberately conservative: a token
/// that is not a complete code is ignored. Only identical source tokens are
/// deduplicated; co-located URIs may carry different uncertainty or altitude.
pub fn parse_codes(
text : String,
max_results~ : Int32 = 100,
) -> Array[Location] {
let limit = if max_results < 1 { 1 } else { max_results }
let out : Array[Location] = []
let seen : Array[String] = []
for raw in split_whitespace(text) {
if out.length() >= limit {
break
}
let token = strip_code_punctuation(raw)
if token == "" {
continue
}
// Short English words such as "meet" and "send" are legal geohash strings,
// and a bare number such as "12345" can also decode as one. In free text,
// require both a digit and a letter before treating a token as a geohash.
if !has_digit(token) && !token.contains("+") {
continue
}
match parse_location(token) {
None => ()
Some(found) => {
if found.format == GeoHash && (token.length() < 5 || !has_letter(token)) {
continue
}
let key = found.format.name() + "|" + found.raw
if index_of_string(seen, key) < 0 {
seen.push(key)
out.push(found)
}
}
}
}
out
}
///|
/// Remove punctuation that commonly wraps a code in prose (`(`, `,`, `.`,
/// quotes) without touching the characters inside it.
fn strip_code_punctuation(text : String) -> String {
let chars = chars_of(text)
let mut start = 0
let mut end = chars.length()
while start < end && is_code_punctuation(chars[start]) {
start = start + 1
}
while end > start && is_code_punctuation(chars[end - 1]) {
end = end - 1
}
let out : Array[Char] = []
for i = start; i < end; i = i + 1 {
out.push(chars[i])
}
string_of(out)
}
///|
fn has_digit(text : String) -> Bool {
for ch in text {
if ch >= '0' && ch <= '9' {
return true
}
}
false
}
///|
fn has_letter(text : String) -> Bool {
for ch in text {
if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') {
return true
}
}
false
}
///|
fn is_code_punctuation(c : Char) -> Bool {
c == ',' ||
c == ';' ||
c == ':' ||
c == '!' ||
c == '?' ||
c == '(' ||
c == ')' ||
c == '[' ||
c == ']' ||
c == '{' ||
c == '}' ||
c == '<' ||
c == '>' ||
c == '"' ||
c == '\'' ||
c == '.'
}
///|
fn index_of_string(values : Array[String], needle : String) -> Int32 {
let mut i = 0
while i < values.length() {
if values[i] == needle {
return i
}
i = i + 1
}
-1
}
///|
pub extend LocationFormat with Eq::{not_equal, equal}
///|
pub extend LocationFormat with @moonbitlang/core/debug.Debug::{to_repr}
///|
pub extend LocationFormat with Show::{output, to_string}