///|
/// GPX 1.0 解析,并升级为 1.1 数据模型。
///
/// GPX 1.0(2004)与 1.1 不兼容,而且**不是简单的子集关系**:
///
/// | 差异 | GPX 1.0 | GPX 1.1 |
/// |---|---|---|
/// | 元数据位置 | `name` `desc` `author` `email` `url` `urlname` `time` `keywords` `bounds` **直接挂在根下** | 全部包在 `` 里 |
/// | 链接 | `` + `` | `` + `` |
/// | 作者 | `` 与 `` 都是纯文本 | `personType`(含 name / email / link) |
/// | 邮箱 | 完整地址 `me@example.com` | 拆成 `id` + `domain` 两个必填属性 |
/// | 朝向 / 速度 | `` `` 是 trkpt 的正式子元素 | 没有(改放 `extensions`) |
/// | 命名空间 | `.../GPX/1/0` | `.../GPX/1/1` |
///
/// 因为差异是"字段换了一套"而不是"少几个字段",本模块**不复用 1.1 的解析函数**,
/// 而是独立解析后统一产出 1.1 模型。理由:复用会让 1.1 的每个解析函数都拖着
/// 版本分支,既脏又容易在改 1.0 时误伤 1.1。
///
/// 迁移是**单向**的(1.0 → 1.1)。反向 1.1 → 1.0 会丢信息
/// (`extensions`、`fix`/`sat` 等精度字段无处安放),本库不提供。
// ── 1.0 的子元素顺序表 ──
//
// 都来自 GPX 1.0 官方 schema 的 sequence 定义。
///|
let root_ranks_1_0 : Array[(String, Int)] = [
("name", 0),
("desc", 1),
("author", 2),
("email", 3),
("url", 4),
("urlname", 5),
("time", 6),
("keywords", 7),
("bounds", 8),
("wpt", 9),
("rte", 10),
("trk", 11),
]
///|
/// 1.0 的 `wpt` / `rtept`:位置 → 描述,没有精度段。
let wpt_ranks_1_0 : Array[(String, Int)] = [
("ele", 0),
("time", 1),
("name", 2),
("cmt", 3),
("desc", 4),
("src", 5),
("url", 6),
("urlname", 7),
("sym", 8),
("type", 9),
]
///|
/// 1.0 的 `trkpt` 比 `wpt` 更短,而且多了 `course` / `speed`。
let trkpt_ranks_1_0 : Array[(String, Int)] = [
("ele", 0),
("time", 1),
("course", 2),
("speed", 3),
]
///|
/// 1.0 的 `rte` / `trk` 共有段。
let route_like_ranks_1_0 : Array[(String, Int)] = [
("name", 0),
("cmt", 1),
("desc", 2),
("src", 3),
("url", 4),
("urlname", 5),
("number", 6),
("rtept", 7),
("trkseg", 7),
]
///|
fn lat_attr(node : XNode, diags : Array[Diagnostic]) -> Latitude? {
match require_attr(node, "lat", diags) {
Some(a) =>
match Latitude::from_string(a.value) {
Ok(v) => Some(v)
Err(e) => {
diags.push(
Diagnostic::error(
code_invalid_decimal,
"属性 lat:\{e.message()}",
a.line,
a.column,
),
)
None
}
}
None => None
}
}
///|
fn lon_attr(node : XNode, diags : Array[Diagnostic]) -> Longitude? {
match require_attr(node, "lon", diags) {
Some(a) =>
match Longitude::from_string(a.value) {
Ok(v) => Some(v)
Err(e) => {
diags.push(
Diagnostic::error(
code_invalid_decimal,
"属性 lon:\{e.message()}",
a.line,
a.column,
),
)
None
}
}
None => None
}
}
///|
/// 1.0 的 `` + `` → 1.1 的 `link`。
///
/// 两个元素都可选,但**只要有一个存在就产出一个 link**——
/// `urlname` 单独出现(没写 url)时 href 为空串,这比丢掉它更符合"无损"原则。
fn links_from_url(node : XNode, links : Array[Link]) -> Array[Link] {
let url = simple_text_child(node, "url")
let urlname = simple_text_child(node, "urlname")
match (url, urlname) {
(None, None) => links
(u, n) => {
let href = match u {
Some(v) => v
None => ""
}
links.push({ href, text: n, mime_type: None, })
links
}
}
}
///|
fn parse_waypoint_1_0(
node : XNode,
diags : Array[Diagnostic],
ranks : Array[(String, Int)],
) -> Waypoint? {
check_order(
node.children,
ranks,
diags,
code_wpt_order,
"<\{node.name}> 的子元素顺序",
)
match lat_attr(node, diags) {
None => None
Some(lat) =>
match lon_attr(node, diags) {
None => None
Some(lon) => {
let links : Array[Link] = []
let links = links_from_url(node, links)
// 1.0 的 trkpt 有 course / speed,1.1 模型没有这两个字段,
// 按 1.1 的通行做法写进 extensions(原始文本,可无损往返)
let course = decimal_child(node, "course", diags)
let speed = decimal_child(node, "speed", diags)
let extensions = course_speed_extensions(course, speed)
Some({
lat,
lon,
ele: decimal_child(node, "ele", diags),
time: datetime_child(node, "time", diags),
magvar: None,
geoid_height: None,
name: simple_text_child(node, "name"),
cmt: simple_text_child(node, "cmt"),
desc: simple_text_child(node, "desc"),
src: simple_text_child(node, "src"),
links,
sym: simple_text_child(node, "sym"),
wpt_type: simple_text_child(node, "type"),
fix: None,
sat: None,
hdop: None,
vdop: None,
pdop: None,
age_of_dgps_data: None,
dgps_id: None,
extensions,
})
}
}
}
}
///|
/// 把 `course` / `speed` 拼成 1.1 的 `extensions` 原文。
fn course_speed_extensions(course : Double?, speed : Double?) -> String? {
match (course, speed) {
(None, None) => None
(Some(c), None) => Some("" + format_decimal(c) + "")
(None, Some(s)) => Some("" + format_decimal(s) + "")
(Some(c), Some(s)) =>
Some(
"" +
format_decimal(c) +
"" +
format_decimal(s) +
"",
)
}
}
///|
fn parse_route_1_0(node : XNode, diags : Array[Diagnostic]) -> Route {
check_order(
node.children,
route_like_ranks_1_0,
diags,
code_rte_order,
" 的子元素顺序",
)
let points : Array[Waypoint] = []
for c in children_named(node, "rtept") {
match parse_waypoint_1_0(c, diags, wpt_ranks_1_0) {
Some(p) => points.push(p)
None => ()
}
}
let links : Array[Link] = []
let links = links_from_url(node, links)
{
name: simple_text_child(node, "name"),
cmt: simple_text_child(node, "cmt"),
desc: simple_text_child(node, "desc"),
src: simple_text_child(node, "src"),
links,
number: match simple_text_child(node, "number") {
Some(t) => parse_nonneg_int(t)
None => None
},
route_type: simple_text_child(node, "type"),
extensions: None,
points,
}
}
///|
fn parse_track_1_0(node : XNode, diags : Array[Diagnostic]) -> Track {
check_order(
node.children,
route_like_ranks_1_0,
diags,
code_trk_order,
" 的子元素顺序",
)
let segments : Array[TrackSegment] = []
for c in children_named(node, "trkseg") {
let points : Array[Waypoint] = []
for p in children_named(c, "trkpt") {
match parse_waypoint_1_0(p, diags, trkpt_ranks_1_0) {
Some(w) => points.push(w)
None => ()
}
}
segments.push({ points, extensions: None, })
}
let links : Array[Link] = []
let links = links_from_url(node, links)
{
name: simple_text_child(node, "name"),
cmt: simple_text_child(node, "cmt"),
desc: simple_text_child(node, "desc"),
src: simple_text_child(node, "src"),
links,
number: match simple_text_child(node, "number") {
Some(t) => parse_nonneg_int(t)
None => None
},
track_type: simple_text_child(node, "type"),
extensions: None,
segments,
}
}
///|
/// 1.0 的邮箱是完整地址文本,1.1 拆成 `id` + `domain` 两个必填属性。
///
/// 按**第一个** `@` 拆分。没有 `@` 时整个串当 id、domain 留空——
/// 这是非法邮箱,但拆不出来的部分不该被静默丢弃。
fn parse_email_1_0(text : String) -> Email {
// 逐字符找第一个 @。不用切片是因为 String 的下标是字节序,
// 而 Char 与字节不一定一一对应
let chars : Array[Char] = []
for ch in text {
chars.push(ch)
}
let mut at = -1
let mut i = 0
while i < chars.length() && at < 0 {
if chars[i] == '@' {
at = i
}
i = i + 1
}
if at < 0 {
return { id: text, domain: "", }
}
let id_chars : Array[Char] = []
let domain_chars : Array[Char] = []
let mut j = 0
while j < at {
id_chars.push(chars[j])
j = j + 1
}
j = at + 1
while j < chars.length() {
domain_chars.push(chars[j])
j = j + 1
}
{
id: String::from_array(id_chars),
domain: String::from_array(domain_chars),
}
}
///|
/// 从 1.0 根元素收集元数据,产出 1.1 的 `Metadata`。
fn parse_metadata_1_0(root : XNode, diags : Array[Diagnostic]) -> Metadata {
let email = match child_named(root, "email") {
Some(c) =>
match text_of(c) {
Some(t) => Some(parse_email_1_0(t))
None => None
}
None => None
}
// 1.0 的 是纯文本,拆到 1.1 的 personType.name
let author = match child_named(root, "author") {
Some(c) =>
match text_of(c) {
Some(t) => Some({ name: Some(t), email, link: None, })
None => None
}
None =>
match email {
// 只有邮箱没有作者名时,也建一个 person 把邮箱挂上去
Some(_) => Some({ name: None, email, link: None, })
None => None
}
}
let links : Array[Link] = []
let links = links_from_url(root, links)
{
name: simple_text_child(root, "name"),
desc: simple_text_child(root, "desc"),
author,
copyright: None,
links,
time: datetime_child(root, "time", diags),
keywords: simple_text_child(root, "keywords"),
bounds: match child_named(root, "bounds") {
Some(c) => parse_bounds(c, diags)
None => None
},
extensions: None,
}
}
///|
/// 探测文档的 `version` 属性值。无法解析时返回 `None`。
///
/// 只用来做入口分派,不校验取值是否合法——那是各解析入口的事。
pub fn detect_gpx_version(src : String) -> String? {
let events = @xml.Reader::from_string(src).read_events_until_eof() catch {
_ => return None
}
let roots = build_xml_tree(events)
if roots.length() == 0 {
return None
}
match attr_of(roots[0], "version") {
Some(a) => Some(a.value)
None => None
}
}
///|
/// 解析 GPX 1.0 文档,并升级为 1.1 数据模型。
///
/// 返回的 `Gpx.version` 固定是 `"1.1"`——**迁移是单向的**。
/// 想原样输出 1.0 请用别的工具,本库不做 1.1 → 1.0(会丢 `extensions`
/// 与精度字段)。
///
/// 与 `parse_gpx` 一样:成功路径上的 warning 会被丢弃,
/// 要完整诊断用 `validate_gpx`。
pub fn parse_gpx_1_0(src : String) -> Result[Gpx, Array[Diagnostic]] {
let diags : Array[Diagnostic] = []
let events = @xml.Reader::from_string(src).read_events_until_eof() catch {
@xml.XmlError::At(error=kind, span~) => {
diags.push(
Diagnostic::error(
code_xml_parse_error,
"XML 解析失败:\{kind.to_string()}",
span.start.line,
span.start.column,
),
)
return Err(diags)
}
}
let roots = build_xml_tree(events)
if roots.length() == 0 {
diags.push(
Diagnostic::error(code_root_not_gpx, "文档没有根元素", 1, 1),
)
return Err(diags)
}
let root = roots[0]
if root.name != "gpx" {
diags.push(
Diagnostic::error(
code_root_not_gpx,
"根元素必须是 ,实际是 <\{root.name}>",
root.line,
root.column,
),
)
return Err(diags)
}
match attr_of(root, "version") {
Some(a) =>
if a.value != "1.0" {
diags.push(
Diagnostic::error(
code_version_not_1_0,
"version 必须是 \"1.0\"(GPX 1.0 文档),实际是 \"\{a.value}\"",
a.line,
a.column,
),
)
}
None =>
diags.push(
Diagnostic::error(
code_version_missing,
" 缺少必填属性 version",
root.line,
root.column,
),
)
}
let creator = match attr_of(root, "creator") {
Some(a) => a.value
None => {
diags.push(
Diagnostic::error(
code_creator_missing,
" 缺少必填属性 creator(创建该文档的软件名称或 URL)",
root.line,
root.column,
),
)
""
}
}
check_order(
root.children,
root_ranks_1_0,
diags,
code_root_order,
" 的子元素顺序",
)
if has_errors(diags) {
return Err(diags)
}
let waypoints : Array[Waypoint] = []
for c in children_named(root, "wpt") {
match parse_waypoint_1_0(c, diags, wpt_ranks_1_0) {
Some(w) => waypoints.push(w)
None => ()
}
}
let routes : Array[Route] = []
for c in children_named(root, "rte") {
routes.push(parse_route_1_0(c, diags))
}
let tracks : Array[Track] = []
for c in children_named(root, "trk") {
tracks.push(parse_track_1_0(c, diags))
}
if has_errors(diags) {
return Err(diags)
}
Ok({
version: "1.1",
creator,
metadata: Some(parse_metadata_1_0(root, diags)),
waypoints,
routes,
tracks,
extensions: None,
})
}
///|
/// 自动探测版本:1.0 走 `parse_gpx_1_0`,否则走 `parse_gpx`(1.1)。
///
/// 探测依据是根元素的 `version` 属性。拿不到(比如 XML 本身坏了)
/// 时退化为按 1.1 解析,由 1.1 的逻辑报出真正的问题。
pub fn parse_gpx_any(src : String) -> Result[Gpx, Array[Diagnostic]] {
match detect_gpx_version(src) {
Some("1.0") => parse_gpx_1_0(src)
_ => parse_gpx(src)
}
}