// 文档校验:收集结构性/取值范围问题,不抛错。
//
// 面向真实数据的质检:坐标越界、环未闭合、顶点不足、样式引用悬空、
// gx:Track 时间戳与坐标数量不一致等。
///|
/// 一条校验问题:出问题的 Placemark 标识 + 说明。
pub(all) struct ValidationIssue {
placemark : String
message : String
}
///|
/// 坐标是否在合法范围内。
fn valid_position(p : KmlPoint) -> Bool {
!p.lon.is_nan() &&
!p.lat.is_nan() &&
!p.lon.is_inf() &&
!p.lat.is_inf() &&
p.lon >= -180.0 &&
p.lon <= 180.0 &&
p.lat >= -90.0 &&
p.lat <= 90.0
}
///|
/// 两个点是否相同(按 1e-9 容差)。
fn same_point(a : KmlPoint, b : KmlPoint) -> Bool {
(a.lon - b.lon).abs() <= 1.0e-9 && (a.lat - b.lat).abs() <= 1.0e-9
}
///|
/// 校验一个点列,把问题追加进 out。
fn validate_points(
label : String,
kind : String,
points : Array[KmlPoint],
min_count : Int32,
out : Array[ValidationIssue],
) -> Unit {
if points.length() < min_count {
out.push({
placemark: label,
message: kind +
" 顶点不足:需要 " +
min_count.to_string() +
" 个,实际 " +
points.length().to_string(),
})
}
let mut i = 0
while i < points.length() {
if !valid_position(points[i]) {
out.push({
placemark: label,
message: kind +
" 第 " +
i.to_string() +
" 个坐标越界或非有限值",
})
}
i = i + 1
}
}
///|
/// 校验一个环。
fn validate_ring(
label : String,
kind : String,
ring : LinearRing,
out : Array[ValidationIssue],
) -> Unit {
validate_points(label, kind, ring.points, 4, out)
if ring.points.length() >= 2 {
let first = ring.points[0]
let last = ring.points[ring.points.length() - 1]
if !same_point(first, last) {
out.push({ placemark: label, message: kind + " 首尾点不闭合", })
}
}
}
///|
/// 校验一个几何。
fn validate_geometry(
label : String,
geometry : Geometry,
out : Array[ValidationIssue],
) -> Unit {
match geometry {
Geometry::Point(p) =>
if !valid_position(p) {
out.push({
placemark: label,
message: "Point 坐标越界或非有限值",
})
}
Geometry::LineString(ls) =>
validate_points(label, "LineString", ls.points, 2, out)
Geometry::Polygon(poly) => {
validate_ring(label, "Polygon 外环", poly.outer, out)
let mut i = 0
while i < poly.inner.length() {
validate_ring(
label,
"Polygon 内环 " + i.to_string(),
poly.inner[i],
out,
)
i = i + 1
}
}
Geometry::MultiGeometry(parts) => {
let mut i = 0
while i < parts.length() {
validate_geometry(label, parts[i], out)
i = i + 1
}
}
Geometry::GxTrack(track) => {
validate_points(label, "gx:Track", track.coords, 1, out)
if track.whens.length() > 0 &&
track.whens.length() != track.coords.length() {
out.push({
placemark: label,
message: "gx:Track 时间戳数 " +
track.whens.length().to_string() +
" 与坐标数 " +
track.coords.length().to_string() +
" 不一致",
})
}
}
}
}
///|
/// 校验整个文档,返回全部问题(空数组表示通过)。
pub fn validate(doc : KmlDocument) -> Array[ValidationIssue] {
let out : Array[ValidationIssue] = []
let mut i = 0
while i < doc.placemarks.length() {
let pm = doc.placemarks[i]
let label = if pm.name != "" {
pm.name
} else {
"placemark[" + i.to_string() + "]"
}
match pm.geometry {
Some(g) => validate_geometry(label, g, out)
None => out.push({ placemark: label, message: "没有几何", })
}
if pm.style_url != "" && pm.style_url.has_prefix("#") {
let id = strip_hash(pm.style_url)
if doc.find_style(id) is None && doc.find_style_map(id) is None {
out.push({
placemark: label,
message: "样式引用悬空:" + pm.style_url,
})
}
}
i = i + 1
}
out
}
///|
/// 去掉 styleUrl 的前导 #。
fn strip_hash(url : String) -> String {
let data = @utf8.encode(url)
if data.length() > 0 && data[0] == b'#' {
@utf8.decode_lossy(data.clamped_view(start=1, end=data.length()))
} else {
url
}
}