///|
/// GPX 解析:XML 事件流 → GPX 数据模型。
///
/// 分两步:
/// 1. 把 XML 事件流搭成轻量节点树(保留源码偏移,用于切 `extensions` 原文)
/// 2. 把节点树映射为 GPX 模型,同时校验 schema 约束
///
/// 所有约束违例都进诊断列表,**不抛异常**——一个文件里可能有多个问题,
/// 调用方需要一次拿到全部。
///|
/// 带源码位置的属性。
priv struct XAttr {
name : String
value : String
line : Int
column : Int
}
///|
/// 轻量 XML 节点。
priv struct XNode {
name : String
attrs : Array[XAttr]
children : Array[XNode]
texts : Array[String]
line : Int
column : Int
/// 起始标签**结束**处的 offset(UTF-16 码元),用于切内部原文
open_end : Int
/// 结束标签**开始**处的 offset;自闭合元素等于 `open_end`
close_start : Int
}
///|
/// 构建中的栈元素。
priv struct PendingNode {
name : String
attrs : Array[XAttr]
children : Array[XNode]
texts : Array[String]
line : Int
column : Int
open_end : Int
}
///|
/// 单个 `Char` 占几个 UTF-16 码元。
///
/// ⚠️ 关键:`@xml` 的 `offset` 是 **UTF-16 码元**偏移,不是码点偏移
/// (实测:含 emoji 的文本 `String::length()` 与最终 offset 一致,而码点数少 1)。
/// 按码点切原文会切出乱码。
fn u16_width(ch : Char) -> Int {
if ch.to_int() > 0xFFFF {
2
} else {
1
}
}
///|
/// 按 UTF-16 码元偏移切出子串。
fn slice_u16(s : String, a : Int, b : Int) -> String {
if a >= b {
return ""
}
let out : Array[Char] = []
let mut pos = 0
for ch in s {
let w = u16_width(ch)
if pos >= a && pos + w <= b {
out.push(ch)
}
pos = pos + w
}
String::from_array(out)
}
///|
fn to_xattrs(el : @xml.XmlElement) -> Array[XAttr] {
let out : Array[XAttr] = []
for a in el.attributes {
out.push({
name: a.name,
value: a.value,
line: a.span.start.line,
column: a.span.start.column,
})
}
out
}
///|
/// 事件流 → 节点树。
fn build_xml_tree(events : Array[@xml.Event]) -> Array[XNode] {
let roots : Array[XNode] = []
let stack : Array[PendingNode] = []
for ev in events {
match ev.kind {
@xml.EventKind::Start(el) =>
stack.push({
name: el.name,
attrs: to_xattrs(el),
children: [],
texts: [],
line: ev.span.start.line,
column: ev.span.start.column,
open_end: ev.span.end.offset,
})
@xml.EventKind::Empty(el) => {
let node : XNode = {
name: el.name,
attrs: to_xattrs(el),
children: [],
texts: [],
line: ev.span.start.line,
column: ev.span.start.column,
open_end: ev.span.start.offset,
close_start: ev.span.start.offset,
}
if stack.length() == 0 {
roots.push(node)
} else {
stack[stack.length() - 1].children.push(node)
}
}
@xml.EventKind::End(_) =>
if stack.length() > 0 {
let top = stack[stack.length() - 1]
let _ = stack.pop()
let node : XNode = {
name: top.name,
attrs: top.attrs,
children: top.children,
texts: top.texts,
line: top.line,
column: top.column,
open_end: top.open_end,
close_start: ev.span.start.offset,
}
if stack.length() == 0 {
roots.push(node)
} else {
stack[stack.length() - 1].children.push(node)
}
}
@xml.EventKind::Text(t) =>
if stack.length() > 0 {
stack[stack.length() - 1].texts.push(t)
}
@xml.EventKind::CData(t) =>
if stack.length() > 0 {
stack[stack.length() - 1].texts.push(t)
}
_ => ()
}
}
roots
}
///|
fn attr_of(node : XNode, name : String) -> XAttr? {
for a in node.attrs {
if a.name == name {
return Some(a)
}
}
None
}
///|
fn child_named(node : XNode, name : String) -> XNode? {
for c in node.children {
if c.name == name {
return Some(c)
}
}
None
}
///|
fn children_named(node : XNode, name : String) -> Array[XNode] {
let out : Array[XNode] = []
for c in node.children {
if c.name == name {
out.push(c)
}
}
out
}
///|
/// 元素的文本内容(直接子文本节点拼接)。空元素返回 `None`。
fn text_of(node : XNode) -> String? {
if node.texts.length() == 0 {
return None
}
Some(node.texts.join(""))
}
///|
/// `extensions` 的**原始内部 XML**,用于无损往返。
fn raw_inner(src : String, node : XNode) -> String {
slice_u16(src, node.open_end, node.close_start)
}
///|
/// 按 schema 的 `xsd:sequence` 校验子元素排列。
///
/// 规则:把每个元素名映射到一个序号,序号必须**单调不减**。
/// 这样既能拦住真正的乱序,又能允许 `link*` 这类重复出现的元素。
fn check_order(
children : Array[XNode],
ranks : Array[(String, Int)],
diags : Array[Diagnostic],
code : String,
context : String,
) -> Unit {
let mut max_rank = -1
let mut max_name = ""
for c in children {
let mut rank : Int? = None
for pair in ranks {
if pair.0 == c.name {
rank = Some(pair.1)
}
}
match rank {
Some(r) =>
if r < max_rank {
diags.push(
Diagnostic::error(
code,
"元素 <\{c.name}> 出现在 <\{max_name}> 之后,违反 schema 规定的顺序(\{context})",
c.line,
c.column,
),
)
} else {
max_rank = r
max_name = c.name
}
None => ()
}
}
}
///|
fn require_attr(
node : XNode,
name : String,
diags : Array[Diagnostic],
) -> XAttr? {
match attr_of(node, name) {
Some(a) => Some(a)
None => {
diags.push(
Diagnostic::error(
code_missing_required_attribute,
"<\{node.name}> 缺少必填属性 \{name}",
node.line,
node.column,
),
)
None
}
}
}
///|
fn decimal_attr(
node : XNode,
name : String,
diags : Array[Diagnostic],
) -> Double? {
match require_attr(node, name, diags) {
Some(a) =>
match parse_decimal(a.value) {
Ok(v) => Some(v)
Err(e) => {
diags.push(
Diagnostic::error(
code_invalid_decimal,
"属性 \{name}:\{e.message()}",
a.line,
a.column,
),
)
None
}
}
None => None
}
}
///|
fn decimal_child(
node : XNode,
name : String,
diags : Array[Diagnostic],
) -> Double? {
match child_named(node, name) {
Some(c) =>
match text_of(c) {
Some(t) =>
match parse_decimal(t) {
Ok(v) => Some(v)
Err(e) => {
diags.push(
Diagnostic::error(
code_invalid_decimal,
"<\{name}>:\{e.message()}",
c.line,
c.column,
),
)
None
}
}
None => None
}
None => None
}
}
///|
fn datetime_child(
node : XNode,
name : String,
diags : Array[Diagnostic],
) -> String? {
match child_named(node, name) {
Some(c) =>
match text_of(c) {
Some(t) =>
if is_valid_datetime(t) {
Some(t)
} else {
diags.push(
Diagnostic::error(
code_invalid_datetime,
"<\{name}> 不是合法的 xsd:dateTime:\"\{t}\"",
c.line,
c.column,
),
)
None
}
None => None
}
None => None
}
}
// ─────────────────────────── 复合类型 ───────────────────────────
///|
fn parse_link(node : XNode, diags : Array[Diagnostic]) -> Link? {
match require_attr(node, "href", diags) {
Some(href) =>
Some({
href: href.value,
text: match child_named(node, "text") {
Some(c) => text_of(c)
None => None
},
mime_type: match child_named(node, "type") {
Some(c) => text_of(c)
None => None
},
})
None => None
}
}
///|
fn parse_email(node : XNode, diags : Array[Diagnostic]) -> Email? {
let id = require_attr(node, "id", diags)
let domain = require_attr(node, "domain", diags)
match (id, domain) {
(Some(i), Some(d)) => Some({ id: i.value, domain: d.value, })
_ => None
}
}
///|
fn parse_person(node : XNode, diags : Array[Diagnostic]) -> Person? {
Some({
name: match child_named(node, "name") {
Some(c) => text_of(c)
None => None
},
email: match child_named(node, "email") {
Some(c) => parse_email(c, diags)
None => None
},
link: match child_named(node, "link") {
Some(c) => parse_link(c, diags)
None => None
},
})
}
///|
fn parse_copyright(node : XNode, diags : Array[Diagnostic]) -> Copyright? {
match require_attr(node, "author", diags) {
Some(author) =>
Some({
author: author.value,
year: match child_named(node, "year") {
Some(c) => text_of(c)
None => None
},
license: match child_named(node, "license") {
Some(c) => text_of(c)
None => None
},
})
None => None
}
}
///|
fn parse_bounds(node : XNode, diags : Array[Diagnostic]) -> Bounds? {
let min_lat_v = decimal_attr(node, "minlat", diags)
let min_lon_v = decimal_attr(node, "minlon", diags)
let max_lat_v = decimal_attr(node, "maxlat", diags)
let max_lon_v = decimal_attr(node, "maxlon", diags)
let min_lat = match min_lat_v {
Some(v) =>
match Latitude::from_double(v) {
Ok(x) => Some(x)
Err(e) => {
diags.push(
Diagnostic::error(
code_invalid_decimal,
"minlat:\{e.message()}",
node.line,
node.column,
),
)
None
}
}
None => None
}
let min_lon = match min_lon_v {
Some(v) =>
match Longitude::from_double(v) {
Ok(x) => Some(x)
Err(e) => {
diags.push(
Diagnostic::error(
code_invalid_decimal,
"minlon:\{e.message()}",
node.line,
node.column,
),
)
None
}
}
None => None
}
let max_lat = match max_lat_v {
Some(v) =>
match Latitude::from_double(v) {
Ok(x) => Some(x)
Err(e) => {
diags.push(
Diagnostic::error(
code_invalid_decimal,
"maxlat:\{e.message()}",
node.line,
node.column,
),
)
None
}
}
None => None
}
let max_lon = match max_lon_v {
Some(v) =>
match Longitude::from_double(v) {
Ok(x) => Some(x)
Err(e) => {
diags.push(
Diagnostic::error(
code_invalid_decimal,
"maxlon:\{e.message()}",
node.line,
node.column,
),
)
None
}
}
None => None
}
match (min_lat, min_lon, max_lat, max_lon) {
(Some(a), Some(b), Some(c), Some(d)) => {
// schema 不强制 min ≤ max,但语义上必须成立 —— 作为警告报告
if a.to_double() > c.to_double() {
diags.push(
Diagnostic::warning(
code_bounds_order,
"bounds 的 minlat(\{a.to_gpx_string()}) 大于 maxlat(\{c.to_gpx_string()})",
node.line,
node.column,
),
)
}
if b.to_double() > d.to_double() {
diags.push(
Diagnostic::warning(
code_bounds_order,
"bounds 的 minlon(\{b.to_gpx_string()}) 大于 maxlon(\{d.to_gpx_string()})",
node.line,
node.column,
),
)
}
Some({ min_lat: a, min_lon: b, max_lat: c, max_lon: d, })
}
_ => None
}
}
///|
/// `metadata` 子元素顺序。
let metadata_ranks : Array[(String, Int)] = [
("name", 0),
("desc", 1),
("author", 2),
("copyright", 3),
("link", 4),
("time", 5),
("keywords", 6),
("bounds", 7),
("extensions", 8),
]
///|
fn parse_metadata(
src : String,
node : XNode,
diags : Array[Diagnostic],
) -> Metadata {
check_order(
node.children,
metadata_ranks,
diags,
code_metadata_order,
"metadata 的子元素顺序",
)
{
name: match child_named(node, "name") {
Some(c) => text_of(c)
None => None
},
desc: match child_named(node, "desc") {
Some(c) => text_of(c)
None => None
},
author: match child_named(node, "author") {
Some(c) => parse_person(c, diags)
None => None
},
copyright: match child_named(node, "copyright") {
Some(c) => parse_copyright(c, diags)
None => None
},
links: {
let out : Array[Link] = []
for c in children_named(node, "link") {
match parse_link(c, diags) {
Some(l) => out.push(l)
None => ()
}
}
out
},
time: datetime_child(node, "time", diags),
keywords: match child_named(node, "keywords") {
Some(c) => text_of(c)
None => None
},
bounds: match child_named(node, "bounds") {
Some(c) => parse_bounds(c, diags)
None => None
},
extensions: match child_named(node, "extensions") {
Some(c) => Some(raw_inner(src, c))
None => None
},
}
}
///|
/// `wptType` 子元素顺序(三段:位置 → 描述 → 精度)。
let wpt_ranks : Array[(String, Int)] = [
("ele", 0),
("time", 1),
("magvar", 2),
("geoidheight", 3),
("name", 4),
("cmt", 5),
("desc", 6),
("src", 7),
("link", 8),
("sym", 9),
("type", 10),
("fix", 11),
("sat", 12),
("hdop", 13),
("vdop", 14),
("pdop", 15),
("ageofdgpsdata", 16),
("dgpsid", 17),
("extensions", 18),
]
///|
fn simple_text_child(node : XNode, name : String) -> String? {
match child_named(node, name) {
Some(c) => text_of(c)
None => None
}
}
///|
/// 单个点的**语义**检查。这些都不违反 schema,所以一律只报 warning。
///
/// 判定标准:schema 允许 ≠ 合理。校验器的价值就在这一步——
/// 一个能通过 XSD 但自相矛盾的文档,比一个干脆报错的文档更危险。
fn check_waypoint_semantics(
node : XNode,
diags : Array[Diagnostic],
wpt : Waypoint,
bounds : Bounds?,
) -> Unit {
// 海拔:地球表面最深处约 -11000 m,最高点约 9000 m
match wpt.ele {
Some(e) =>
if e > 9000.0 || e < -11000.0 {
diags.push(
Diagnostic::warning(
code_elevation_implausible,
" = \{format_decimal(e)} m 超出地球表面合理范围(约 -11000 ~ 9000 m),请确认单位是否为米",
node.line,
node.column,
),
)
}
None => ()
}
// fix 与 sat 自相矛盾
match (wpt.fix, wpt.sat) {
(Some(f), Some(s)) =>
if f == FixNone && s > 0 {
diags.push(
Diagnostic::warning(
code_fix_sat_conflict,
" 是 none(无定位)但 是 \{s},两者矛盾",
node.line,
node.column,
),
)
} else if f != FixNone && s == 0 {
diags.push(
Diagnostic::warning(
code_fix_sat_conflict,
" 是 \{f.to_string()}(有定位)但 是 0,两者矛盾",
node.line,
node.column,
),
)
}
_ => ()
}
// 点位落在声明的 bounds 之外
match bounds {
Some(b) =>
if wpt.lat.to_double() < b.min_lat.to_double() ||
wpt.lat.to_double() > b.max_lat.to_double() ||
wpt.lon.to_double() < b.min_lon.to_double() ||
wpt.lon.to_double() > b.max_lon.to_double() {
diags.push(
Diagnostic::warning(
code_out_of_bounds,
"点位 (\{wpt.lat.to_gpx_string()}, \{wpt.lon.to_gpx_string()}) 落在 metadata/bounds 声明的范围之外",
node.line,
node.column,
),
)
}
None => ()
}
}
///|
fn parse_waypoint(
src : String,
node : XNode,
diags : Array[Diagnostic],
bounds : Bounds?,
) -> Waypoint? {
check_order(
node.children,
wpt_ranks,
diags,
code_wpt_order,
"<\{node.name}> 的子元素顺序",
)
let lat_a = require_attr(node, "lat", diags)
let lon_a = require_attr(node, "lon", diags)
let lat = match lat_a {
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
}
let lon = match lon_a {
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
}
match (lat, lon) {
(Some(la), Some(lo)) => {
let fix = match simple_text_child(node, "fix") {
Some(t) =>
match FixType::from_string(t) {
Some(f) => Some(f)
None => {
let c = child_named(node, "fix")
match c {
Some(cc) =>
diags.push(
Diagnostic::error(
code_invalid_fix,
" 取值非法:\"\{t}\"(只允许 none / 2d / 3d / dgps / pps,小写)",
cc.line,
cc.column,
),
)
None => ()
}
None
}
}
None => None
}
let magvar = match simple_text_child(node, "magvar") {
Some(t) =>
match Degrees::from_string(t) {
Ok(v) => Some(v)
Err(e) => {
diags.push(
Diagnostic::error(
code_invalid_decimal,
"magvar:\{e.message()}",
node.line,
node.column,
),
)
None
}
}
None => None
}
let dgps_id = match simple_text_child(node, "dgpsid") {
Some(t) =>
match DgpsStationId::from_string(t) {
Ok(v) => Some(v)
Err(e) => {
diags.push(
Diagnostic::error(
code_invalid_decimal,
"dgpsid:\{e.message()}",
node.line,
node.column,
),
)
None
}
}
None => None
}
let sat = match simple_text_child(node, "sat") {
Some(t) =>
match parse_nonneg_int(t) {
Some(v) => Some(v)
None => {
diags.push(
Diagnostic::error(
code_invalid_decimal,
" 必须是非负整数:\"\{t}\"",
node.line,
node.column,
),
)
None
}
}
None => None
}
let wpt : Waypoint = {
lat: la,
lon: lo,
ele: decimal_child(node, "ele", diags),
time: datetime_child(node, "time", diags),
magvar,
geoid_height: decimal_child(node, "geoidheight", diags),
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: {
let out : Array[Link] = []
for c in children_named(node, "link") {
match parse_link(c, diags) {
Some(l) => out.push(l)
None => ()
}
}
out
},
sym: simple_text_child(node, "sym"),
wpt_type: simple_text_child(node, "type"),
fix,
sat,
hdop: decimal_child(node, "hdop", diags),
vdop: decimal_child(node, "vdop", diags),
pdop: decimal_child(node, "pdop", diags),
age_of_dgps_data: decimal_child(node, "ageofdgpsdata", diags),
dgps_id,
extensions: match child_named(node, "extensions") {
Some(c) => Some(raw_inner(src, c))
None => None
},
}
check_waypoint_semantics(node, diags, wpt, bounds)
Some(wpt)
}
_ => None
}
}
///|
fn parse_nonneg_int(text : String) -> Int? {
let s = collapse_space(text)
let codes = codes_of(s)
if codes.length() == 0 {
return None
}
let mut i = 0
while i < codes.length() {
if !is_digit_code(codes[i]) {
return None
}
i = i + 1
}
Some(@string.parse_int(s)) catch {
_ => None
}
}
///|
/// `rte` / `trk` 的公共子元素顺序(两者相同)。
let route_like_ranks : Array[(String, Int)] = [
("name", 0),
("cmt", 1),
("desc", 2),
("src", 3),
("link", 4),
("number", 5),
("type", 6),
("extensions", 7),
("rtept", 8),
("trkseg", 8),
]
///|
fn parse_route(
src : String,
node : XNode,
diags : Array[Diagnostic],
bounds : Bounds?,
) -> Route {
check_order(
node.children,
route_like_ranks,
diags,
code_rte_order,
"rte 的子元素顺序",
)
let route_points : Array[Waypoint] = []
for c in children_named(node, "rtept") {
match parse_waypoint(src, c, diags, bounds) {
Some(w) => route_points.push(w)
None => ()
}
}
if route_points.length() == 0 {
diags.push(
Diagnostic::warning(
code_empty_segment,
" 没有任何 ",
node.line,
node.column,
),
)
}
{
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: {
let out : Array[Link] = []
for c in children_named(node, "link") {
match parse_link(c, diags) {
Some(l) => out.push(l)
None => ()
}
}
out
},
number: match simple_text_child(node, "number") {
Some(t) => parse_nonneg_int(t)
None => None
},
route_type: simple_text_child(node, "type"),
extensions: match child_named(node, "extensions") {
Some(c) => Some(raw_inner(src, c))
None => None
},
points: route_points,
}
}
///|
fn parse_track_segment(
src : String,
node : XNode,
diags : Array[Diagnostic],
bounds : Bounds?,
) -> TrackSegment {
let points : Array[Waypoint] = []
let mut prev_epoch : Double? = None
let mut prev_wpt : Waypoint? = None
for c in children_named(node, "trkpt") {
match parse_waypoint(src, c, diags, bounds) {
Some(wpt) => {
// 时间单调性:同一段内时间应递增
match wpt.time {
Some(t) =>
match datetime_to_epoch(t) {
Some(e) => {
match prev_epoch {
Some(pe) =>
if e < pe {
diags.push(
Diagnostic::warning(
code_time_not_monotonic,
"轨迹点时间 \{t} 早于上一个点,同一 trkseg 内时间应递增",
c.line,
c.column,
),
)
}
None => ()
}
prev_epoch = Some(e)
}
None => ()
}
None => ()
}
// 相邻点完全重合:疑似重复记录或 GPS 漂移
match prev_wpt {
Some(pw) =>
if pw.lat == wpt.lat && pw.lon == wpt.lon && pw.ele == wpt.ele {
diags.push(
Diagnostic::warning(
code_duplicate_point,
"相邻轨迹点完全相同(lat/lon/ele 一致),疑似重复记录或 GPS 漂移",
c.line,
c.column,
),
)
}
None => ()
}
prev_wpt = Some(wpt)
points.push(wpt)
}
None => ()
}
}
if points.length() == 0 {
diags.push(
Diagnostic::warning(
code_empty_segment,
" 没有任何 ",
node.line,
node.column,
),
)
}
{
points,
extensions: match child_named(node, "extensions") {
Some(c) => Some(raw_inner(src, c))
None => None
},
}
}
///|
fn parse_track(
src : String,
node : XNode,
diags : Array[Diagnostic],
bounds : Bounds?,
) -> Track {
check_order(
node.children,
route_like_ranks,
diags,
code_trk_order,
"trk 的子元素顺序",
)
let segments : Array[TrackSegment] = []
for c in children_named(node, "trkseg") {
segments.push(parse_track_segment(src, c, diags, bounds))
}
if segments.length() == 0 {
diags.push(
Diagnostic::warning(
code_empty_segment,
" 没有任何 ",
node.line,
node.column,
),
)
}
{
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: {
let out : Array[Link] = []
for c in children_named(node, "link") {
match parse_link(c, diags) {
Some(l) => out.push(l)
None => ()
}
}
out
},
number: match simple_text_child(node, "number") {
Some(t) => parse_nonneg_int(t)
None => None
},
track_type: simple_text_child(node, "type"),
extensions: match child_named(node, "extensions") {
Some(c) => Some(raw_inner(src, c))
None => None
},
segments,
}
}
///|
/// 根元素子元素顺序。
let root_ranks : Array[(String, Int)] = [
("metadata", 0),
("wpt", 1),
("rte", 2),
("trk", 3),
("extensions", 4),
]
///|
/// 解析 + 校验的核心:一次性跑完**所有**检查,中途不因错误而停止。
///
/// 返回 `(模型?, 全部诊断)`。模型只在没有错误级诊断时可用;
/// 诊断列表**始终包含全部问题**(含 warning),不会因为解析成功就被丢掉。
///
/// 这就是 `parse_gpx`(要结果、只要错误)与 `validate_gpx`(不要结果、要全部)
/// 两个公开入口的共同底座——同一套逻辑,两种视角。
fn parse_gpx_core(src : String) -> (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 (None, diags)
}
}
let roots = build_xml_tree(events)
if roots.length() == 0 {
diags.push(
Diagnostic::error(code_root_not_gpx, "文档没有根元素", 1, 1),
)
return (None, diags)
}
let root = roots[0]
if root.name != "gpx" {
diags.push(
Diagnostic::error(
code_root_not_gpx,
"根元素必须是 ,实际是 <\{root.name}>",
root.line,
root.column,
),
)
return (None, diags)
}
// version:必填且固定为 "1.1"
let version = match attr_of(root, "version") {
Some(a) => {
if a.value != "1.1" {
diags.push(
Diagnostic::error(
code_version_not_1_1,
"version 必须是 \"1.1\"(schema 固定值),实际是 \"\{a.value}\"",
a.line,
a.column,
),
)
}
a.value
}
None => {
diags.push(
Diagnostic::error(
code_version_missing,
" 缺少必填属性 version",
root.line,
root.column,
),
)
""
}
}
// creator:必填
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,
diags,
code_root_order,
" 的子元素顺序",
)
// metadata 至多一次
let metadata_nodes = children_named(root, "metadata")
if metadata_nodes.length() > 1 {
let second = metadata_nodes[1]
diags.push(
Diagnostic::error(
code_metadata_duplicate,
" 最多只能出现一次",
second.line,
second.column,
),
)
}
let metadata = match metadata_nodes.length() > 0 {
true => Some(parse_metadata(src, metadata_nodes[0], diags))
false => None
}
// bounds 先取出来:点位的越界检查要用它(schema 里 metadata 一定排在 wpt 之前,
// 所以这里拿到的 bounds 对后续所有点都有效)
let bounds = match metadata {
Some(m) => m.bounds
None => None
}
let waypoints : Array[Waypoint] = []
for c in children_named(root, "wpt") {
match parse_waypoint(src, c, diags, bounds) {
Some(w) => waypoints.push(w)
None => ()
}
}
let routes : Array[Route] = []
for c in children_named(root, "rte") {
routes.push(parse_route(src, c, diags, bounds))
}
let tracks : Array[Track] = []
for c in children_named(root, "trk") {
tracks.push(parse_track(src, c, diags, bounds))
}
let gpx : Gpx = {
version,
creator,
metadata,
waypoints,
routes,
tracks,
extensions: match child_named(root, "extensions") {
Some(c) => Some(raw_inner(src, c))
None => None
},
}
if has_errors(diags) {
(None, diags)
} else {
(Some(gpx), diags)
}
}
///|
/// 解析 GPX 文档。
///
/// 返回 `Ok(Gpx)` 表示 XML 结构完整、且没有违反 schema 的问题;
/// 返回 `Err(诊断列表)` 表示存在错误。
///
/// ⚠️ **成功路径上的 warning 会被丢弃**——这是刻意的:`parse_gpx` 要的是
/// "能不能用",不是"完不完美"。想拿到包括 warning 在内的全部诊断,
/// 用 `validate_gpx`。
pub fn parse_gpx(src : String) -> Result[Gpx, Array[Diagnostic]] {
let (g, diags) = parse_gpx_core(src)
if has_errors(diags) {
Err(diags)
} else {
match g {
Some(gpx) => Ok(gpx)
None => Err(diags)
}
}
}