///|
/// GPX 坐标与数值类型。
///
/// 对应 GPX 1.1 schema 的 `latitudeType` / `longitudeType` / `degreesType` /
/// `dgpsStationType` 四个简单类型。
///
/// 规范来源:https://www.topografix.com/GPX/1/1/gpx.xsd
///
/// **最容易被做错的一点**:纬度和经度的边界规则**不对称**——
/// 纬度是闭区间 `[-90, 90]`,经度是半开区间 `[-180, 180)`。
/// `lat="90"` 合法,`lon="180"` 非法。
///|
/// 坐标 / 数值的解析与范围校验失败原因。
///
/// 每个变体都携带**原始文本**,方便调用方直接拿去做报错信息。
pub(all) enum GeoError {
/// 不是合法的 `xsd:decimal` 字面量。
/// 注意:指数记法(`1e2`)、`NaN`、`INF` 都属于**非法**——
/// 它们是 `xsd:double` 的词法形式,不是 `xsd:decimal`。
NotADecimal(String)
/// 纬度超出闭区间 `[-90, 90]`。
LatitudeOutOfRange(String)
/// 经度超出半开区间 `[-180, 180)`。
LongitudeOutOfRange(String)
/// 角度超出半开区间 `[0, 360)`。
DegreesOutOfRange(String)
/// DGPS 基站 ID 超出闭区间 `[0, 1023]`。
DgpsStationOutOfRange(String)
} derive(Eq, Debug)
///|
/// 人类可读的错误描述。
pub fn GeoError::message(self : GeoError) -> String {
match self {
NotADecimal(t) => "not a valid xsd:decimal literal: \{t}"
LatitudeOutOfRange(t) => "latitude out of range [-90, 90]: \{t}"
LongitudeOutOfRange(t) => "longitude out of range [-180, 180): \{t}"
DegreesOutOfRange(t) => "degrees out of range [0, 360): \{t}"
DgpsStationOutOfRange(t) => "dgps station id out of range [0, 1023]: \{t}"
}
}
///|
/// 取字符串的 Unicode 码点数组。
///
/// ⚠️ 不能用 `s.length()` 配合下标——`length()` 按 UTF-16 码元计数,
/// 而迭代按码点,两者混用会在非 BMP 字符上出错。
fn codes_of(s : String) -> Array[Int] {
let arr : Array[Int] = []
for ch in s {
arr.push(ch.to_int())
}
arr
}
///|
fn is_ascii_space(c : Int) -> Bool {
c == 0x20 || c == 0x09 || c == 0x0A || c == 0x0D
}
///|
fn is_digit_code(c : Int) -> Bool {
c >= 0x30 && c <= 0x39
}
///|
/// `xsd:decimal` 的 whitespace facet 是 `collapse`:首尾空白折叠掉。
/// 由于 decimal 本身不含空白字符,这一步等价于 trim。
fn collapse_space(text : String) -> String {
let codes = codes_of(text)
let n = codes.length()
let mut start = 0
let mut end = n
while start < end && is_ascii_space(codes[start]) {
start = start + 1
}
while end > start && is_ascii_space(codes[end - 1]) {
end = end - 1
}
let out : Array[Char] = []
for i = start; i < end; i = i + 1 {
match Int::to_char(codes[i]) {
Some(ch) => out.push(ch)
None => ()
}
}
String::from_array(out)
}
///|
/// 校验码点序列是否符合 `xsd:decimal` 的词法规则:
///
/// ```text
/// (\+|-)? ( [0-9]+ (\.[0-9]*)? | \.[0-9]+ )
/// ```
///
/// 明确**拒绝**:指数记法、`NaN`、`INF`、空串、多个小数点、
/// 只有符号或只有小数点。
fn is_decimal_lexical(codes : Array[Int]) -> Bool {
let n = codes.length()
if n == 0 {
return false
}
let mut i = 0
// 可选符号
if codes[0] == 0x2B || codes[0] == 0x2D {
i = 1
}
if i >= n {
return false
}
// 整数部分
let int_start = i
while i < n && is_digit_code(codes[i]) {
i = i + 1
}
let int_digits = i - int_start
if i == n {
// 纯整数形式,如 "-90"
return int_digits > 0
}
// 必须是小数点
if codes[i] != 0x2E {
return false
}
i = i + 1
// 小数部分
let frac_start = i
while i < n && is_digit_code(codes[i]) {
i = i + 1
}
let frac_digits = i - frac_start
if i != n {
// 小数部分之后还有字符 → 非法(拦住指数记法、NaN 之类)
return false
}
// 整数部分和小数部分至少有一位数字:".5" 合法,"." 非法
int_digits + frac_digits > 0
}
///|
/// 解析 `xsd:decimal` 字面量。
///
/// GPX 的坐标、海拔、各类 dop 值都是这个类型。
pub fn parse_decimal(text : String) -> Result[Double, GeoError] {
let s = collapse_space(text)
if !is_decimal_lexical(codes_of(s)) {
return Err(GeoError::NotADecimal(text))
}
let d = @string.parse_double(s) catch {
_ => return Err(GeoError::NotADecimal(text))
}
if d.is_nan() || d.is_inf() {
return Err(GeoError::NotADecimal(text))
}
Ok(d)
}
///|
fn index_of_e(codes : Array[Int]) -> Int {
let n = codes.length()
let mut i = 0
while i < n {
let c = codes[i]
if c == 0x65 || c == 0x45 {
return i
}
i = i + 1
}
-1
}
///|
fn codes_to_string(codes : Array[Int]) -> String {
let out : Array[Char] = []
for c in codes {
match Int::to_char(c) {
Some(ch) => out.push(ch)
None => ()
}
}
String::from_array(out)
}
///|
/// 把指数记法展开成纯十进制文本。
///
/// 例:`1e-7` → `0.0000001`;`1e+21` → `1000000000000000000000`。
/// 不涉及浮点运算,纯数字字符串搬移。
fn expand_exponent(text : String) -> String {
let codes = codes_of(text)
let epos = index_of_e(codes)
if epos < 0 {
return text
}
// 尾数部分(可能带符号与小数点)
let mantissa = codes[:epos]
// 指数部分
let exp_codes = codes[epos + 1:]
let mut exp = 0
let mut ei = 0
let mut exp_negative = false
if ei < exp_codes.length() && (exp_codes[ei] == 0x2B || exp_codes[ei] == 0x2D) {
exp_negative = exp_codes[ei] == 0x2D
ei = ei + 1
}
while ei < exp_codes.length() && is_digit_code(exp_codes[ei]) {
exp = exp * 10 + (exp_codes[ei] - 0x30)
ei = ei + 1
}
if exp_negative {
exp = -exp
}
// 拆出符号 / 整数位 / 小数位
let mut mi = 0
let mut negative = false
if mi < mantissa.length() && mantissa[mi] == 0x2D {
negative = true
mi = mi + 1
} else if mi < mantissa.length() && mantissa[mi] == 0x2B {
mi = mi + 1
}
let digits : Array[Int] = []
let mut int_len = 0
let mut seen_dot = false
while mi < mantissa.length() {
let c = mantissa[mi]
if c == 0x2E {
seen_dot = true
} else if is_digit_code(c) {
digits.push(c)
if !seen_dot {
int_len = int_len + 1
}
}
mi = mi + 1
}
// 小数点要移动到的位置
let point = int_len + exp
let out : Array[Int] = []
if negative {
out.push(0x2D)
}
if point <= 0 {
out.push(0x30)
out.push(0x2E)
let mut z = 0
while z < -point {
out.push(0x30)
z = z + 1
}
for c in digits {
out.push(c)
}
} else if point >= digits.length() {
for c in digits {
out.push(c)
}
let mut z = digits.length()
while z < point {
out.push(0x30)
z = z + 1
}
} else {
let mut k = 0
while k < point {
out.push(digits[k])
k = k + 1
}
out.push(0x2E)
while k < digits.length() {
out.push(digits[k])
k = k + 1
}
}
codes_to_string(out)
}
///|
/// 把 `Double` 格式化成**合法的 `xsd:decimal` 字面量**。
///
/// ⚠️ 不能直接用 `Double::to_string`:MoonBit 对 `1e-7`、`1e+21` 这类值会输出
/// 指数记法,而 `xsd:decimal` **不允许指数**。直接回写会产出非法 GPX 文档。
/// 这里把指数形式展开成纯十进制。
pub fn format_decimal(v : Double) -> String {
expand_exponent(v.to_string())
}
///|
/// 纬度:**闭区间** `[-90, 90]`,两端都合法。
pub struct Latitude {
value : Double
} derive(Eq, Debug)
///|
pub fn Latitude::from_double(v : Double) -> Result[Latitude, GeoError] {
if v.is_nan() || v.is_inf() || v < -90.0 || v > 90.0 {
return Err(GeoError::LatitudeOutOfRange(format_decimal(v)))
}
Ok({ value: v, })
}
///|
pub fn Latitude::from_string(text : String) -> Result[Latitude, GeoError] {
match parse_decimal(text) {
Ok(v) =>
if v < -90.0 || v > 90.0 {
Err(GeoError::LatitudeOutOfRange(text))
} else {
Ok({ value: v, })
}
Err(e) => Err(e)
}
}
///|
pub fn Latitude::to_double(self : Latitude) -> Double {
self.value
}
///|
pub fn Latitude::to_gpx_string(self : Latitude) -> String {
format_decimal(self.value)
}
///|
/// 经度:**半开区间** `[-180, 180)`——上界排他,`180` 非法。
pub struct Longitude {
value : Double
} derive(Eq, Debug)
///|
pub fn Longitude::from_double(v : Double) -> Result[Longitude, GeoError] {
if v.is_nan() || v.is_inf() || v < -180.0 || v >= 180.0 {
return Err(GeoError::LongitudeOutOfRange(format_decimal(v)))
}
Ok({ value: v, })
}
///|
pub fn Longitude::from_string(text : String) -> Result[Longitude, GeoError] {
match parse_decimal(text) {
Ok(v) =>
if v < -180.0 || v >= 180.0 {
Err(GeoError::LongitudeOutOfRange(text))
} else {
Ok({ value: v, })
}
Err(e) => Err(e)
}
}
///|
pub fn Longitude::to_double(self : Longitude) -> Double {
self.value
}
///|
pub fn Longitude::to_gpx_string(self : Longitude) -> String {
format_decimal(self.value)
}
///|
/// 角度(GPX 里用于 `magvar`):半开区间 `[0, 360)`。
pub struct Degrees {
value : Double
} derive(Eq, Debug)
///|
pub fn Degrees::from_double(v : Double) -> Result[Degrees, GeoError] {
if v.is_nan() || v.is_inf() || v < 0.0 || v >= 360.0 {
return Err(GeoError::DegreesOutOfRange(format_decimal(v)))
}
Ok({ value: v, })
}
///|
pub fn Degrees::from_string(text : String) -> Result[Degrees, GeoError] {
match parse_decimal(text) {
Ok(v) =>
if v < 0.0 || v >= 360.0 {
Err(GeoError::DegreesOutOfRange(text))
} else {
Ok({ value: v, })
}
Err(e) => Err(e)
}
}
///|
pub fn Degrees::to_double(self : Degrees) -> Double {
self.value
}
///|
pub fn Degrees::to_gpx_string(self : Degrees) -> String {
format_decimal(self.value)
}
///|
/// DGPS 基站 ID(GPX 的 `dgpsid`):闭区间 `[0, 1023]` 整数。
pub struct DgpsStationId {
value : Int
} derive(Eq, Debug)
///|
pub fn DgpsStationId::from_int(v : Int) -> Result[DgpsStationId, GeoError] {
if v < 0 || v > 1023 {
return Err(GeoError::DgpsStationOutOfRange(v.to_string()))
}
Ok({ value: v, })
}
///|
/// 从文本解析。必须是十进制整数,不接受 `12.0` 这类形式。
pub fn DgpsStationId::from_string(
text : String,
) -> Result[DgpsStationId, GeoError] {
let s = collapse_space(text)
let codes = codes_of(s)
if codes.length() == 0 {
return Err(GeoError::NotADecimal(text))
}
let mut i = 0
if codes[0] == 0x2B || codes[0] == 0x2D {
i = 1
}
if i >= codes.length() {
return Err(GeoError::NotADecimal(text))
}
let mut seen_digit = false
while i < codes.length() {
if !is_digit_code(codes[i]) {
return Err(GeoError::NotADecimal(text))
}
seen_digit = true
i = i + 1
}
if !seen_digit {
return Err(GeoError::NotADecimal(text))
}
let v = @string.parse_int(s) catch {
_ => return Err(GeoError::NotADecimal(text))
}
DgpsStationId::from_int(v)
}
///|
pub fn DgpsStationId::to_int(self : DgpsStationId) -> Int {
self.value
}
///|
pub fn DgpsStationId::to_gpx_string(self : DgpsStationId) -> String {
self.value.to_string()
}