///|
pub(all) enum TrackFindingKind {
NoFixFinding
LowSatellitesFinding
LowFixQualityFinding
NonMonotonicTimeFinding
LongGapFinding
CoordinateJumpFinding
ExcessiveSpeedFinding
SpeedMismatchFinding
} derive(Eq, Debug)
///|
pub(all) struct TrackFinding {
kind_value : TrackFindingKind
severity_value : Severity
point_index_value : Int?
segment_index_value : Int?
diagnostic_value : Diagnostic
} derive(Eq, Debug)
///|
pub(all) struct TrackQualityReport {
findings_value : Array[TrackFinding]
} derive(Eq, Debug)
///|
pub(all) struct QualityThresholds {
max_gap_millis_value : Int64
max_segment_distance_meters_value : Double
max_speed_mps_value : Double
max_speed_mismatch_mps_value : Double
minimum_satellites_value : Int
minimum_fix_quality_rank_value : Int
} derive(Eq, Debug)
///|
pub(all) struct TrackFilter {
start_time_value : UtcDateTime?
end_time_value : UtcDateTime?
bounds_value : BoundingBox?
minimum_fix_quality_rank_value : Int?
maximum_severity_value : Severity?
} derive(Eq, Debug)
///|
fn track_quality_error(message : String) -> NmeaError {
NmeaError::from_diagnostic(
Diagnostic::new(TrackQualityIssue, Error, message, SourceRef::sentence(0)),
)
}
///|
pub fn QualityThresholds::new(
max_gap_millis? : Int64 = 30000L,
max_segment_distance_meters? : Double = 1000.0,
max_speed_mps? : Double = 100.0,
max_speed_mismatch_mps? : Double = 15.0,
minimum_satellites? : Int = 4,
minimum_fix_quality_rank? : Int = 1,
) -> Result[QualityThresholds, NmeaError] {
if max_gap_millis <= 0L ||
!finite(max_segment_distance_meters) ||
max_segment_distance_meters <= 0.0 ||
!finite(max_speed_mps) ||
max_speed_mps <= 0.0 ||
!finite(max_speed_mismatch_mps) ||
max_speed_mismatch_mps < 0.0 ||
minimum_satellites < 0 ||
minimum_fix_quality_rank < 0 {
return Err(
track_quality_error("quality thresholds are outside valid bounds"),
)
}
Ok({
max_gap_millis_value: max_gap_millis,
max_segment_distance_meters_value: max_segment_distance_meters,
max_speed_mps_value: max_speed_mps,
max_speed_mismatch_mps_value: max_speed_mismatch_mps,
minimum_satellites_value: minimum_satellites,
minimum_fix_quality_rank_value: minimum_fix_quality_rank,
})
}
///|
pub fn QualityThresholds::standard() -> QualityThresholds {
QualityThresholds::new().unwrap()
}
///|
fn fix_quality_rank(quality : GgaFixQuality) -> Int {
match quality {
InvalidFix => 0
GpsFix => 1
DgpsFix => 2
PpsFix => 3
RtkFixed => 5
RtkFloat => 4
EstimatedFix => 1
ManualFix => 1
SimulationFix => 1
UnknownFix(_) => 0
}
}
///|
fn point_source_index(point : TrackPoint, fallback : Int) -> Int {
if point.source_record_indexes_value.length() > 0 {
point.source_record_indexes_value[0]
} else {
fallback
}
}
///|
fn quality_finding(
kind : TrackFindingKind,
severity : Severity,
message : String,
source_index : Int,
point_index : Int?,
segment_index : Int?,
) -> TrackFinding {
{
kind_value: kind,
severity_value: severity,
point_index_value: point_index,
segment_index_value: segment_index,
diagnostic_value: Diagnostic::new(
TrackQualityIssue,
severity,
message,
SourceRef::sentence(source_index),
),
}
}
///|
pub fn analyze_track_quality(
track : Track,
thresholds : QualityThresholds,
) -> TrackQualityReport {
let findings : Array[TrackFinding] = []
for index = 0; index < track.points_value.length(); index = index + 1 {
let point = track.points_value[index]
let source = point_source_index(point, index)
if !point.usable_value {
findings.push(
quality_finding(
NoFixFinding,
Error,
"point is marked as unusable navigation evidence",
source,
Some(index),
None,
),
)
}
match point.satellites_value {
Some(value) if value < thresholds.minimum_satellites_value =>
findings.push(
quality_finding(
LowSatellitesFinding,
Warning,
"point satellite count is below the configured minimum",
source,
Some(index),
None,
),
)
_ => ()
}
match point.quality_value {
Some(value) if fix_quality_rank(value) <
thresholds.minimum_fix_quality_rank_value =>
findings.push(
quality_finding(
LowFixQualityFinding,
Warning,
"point fix quality is below the configured minimum",
source,
Some(index),
None,
),
)
_ => ()
}
}
for index = 0; index < track.segments_value.length(); index = index + 1 {
let segment = track.segments_value[index]
let end_point = track.points_value[segment.end_index_value]
let source = point_source_index(end_point, segment.end_index_value)
match segment.duration_millis_value {
Some(value) if value <= 0L =>
findings.push(
quality_finding(
NonMonotonicTimeFinding,
Error,
"segment time does not increase",
source,
None,
Some(index),
),
)
Some(value) if value > thresholds.max_gap_millis_value =>
findings.push(
quality_finding(
LongGapFinding,
Warning,
"segment duration exceeds the configured gap",
source,
None,
Some(index),
),
)
_ => ()
}
if segment.distance_meters_value >
thresholds.max_segment_distance_meters_value {
findings.push(
quality_finding(
CoordinateJumpFinding,
Warning,
"segment distance exceeds the configured jump",
source,
None,
Some(index),
),
)
}
match segment.derived_speed_mps_value {
Some(value) if value > thresholds.max_speed_mps_value =>
findings.push(
quality_finding(
ExcessiveSpeedFinding,
Error,
"segment derived speed exceeds the configured maximum",
source,
None,
Some(index),
),
)
_ => ()
}
match
(segment.derived_speed_mps_value, end_point.reported_speed_knots_value) {
(Some(derived), Some(reported)) => {
let reported_mps = reported.to_double() * 0.5144444444444445
if (derived - reported_mps).abs() >
thresholds.max_speed_mismatch_mps_value {
findings.push(
quality_finding(
SpeedMismatchFinding,
Warning,
"reported and derived speed differ beyond the configured tolerance",
source,
None,
Some(index),
),
)
}
}
_ => ()
}
}
{ findings_value: findings }
}
///|
pub fn TrackFilter::new(
start_time? : UtcDateTime? = None,
end_time? : UtcDateTime? = None,
bounds? : BoundingBox? = None,
minimum_fix_quality_rank? : Int? = None,
maximum_severity? : Severity? = None,
) -> Result[TrackFilter, NmeaError] {
match (start_time, end_time) {
(Some(start), Some(end)) if start.compare(end) > 0 =>
return Err(track_quality_error("filter start time exceeds end time"))
_ => ()
}
match minimum_fix_quality_rank {
Some(value) if value < 0 =>
return Err(track_quality_error("minimum fix quality rank is negative"))
_ => ()
}
Ok({
start_time_value: start_time,
end_time_value: end_time,
bounds_value: bounds,
minimum_fix_quality_rank_value: minimum_fix_quality_rank,
maximum_severity_value: maximum_severity,
})
}
///|
fn severity_rank(severity : Severity) -> Int {
match severity {
Info => 0
Warning => 1
Error => 2
}
}
///|
fn point_matches_filter(point : TrackPoint, filter : TrackFilter) -> Bool {
match filter.start_time_value {
Some(start) =>
match point.datetime_value {
Some(value) if value.compare(start) >= 0 => ()
_ => return false
}
None => ()
}
match filter.end_time_value {
Some(end) =>
match point.datetime_value {
Some(value) if value.compare(end) <= 0 => ()
_ => return false
}
None => ()
}
match filter.bounds_value {
Some(bounds) =>
if point.latitude_value < bounds.minimum_latitude_value ||
point.latitude_value > bounds.maximum_latitude_value ||
point.longitude_value < bounds.minimum_longitude_value ||
point.longitude_value > bounds.maximum_longitude_value {
return false
}
None => ()
}
match filter.minimum_fix_quality_rank_value {
Some(minimum) =>
match point.quality_value {
Some(quality) if fix_quality_rank(quality) >= minimum => ()
_ => return false
}
None => ()
}
true
}
///|
pub fn filter_track(
track : Track,
filter : TrackFilter,
report? : TrackQualityReport? = None,
) -> Result[Track, NmeaError] {
let excluded = Array::make(track.points_value.length(), false)
match (filter.maximum_severity_value, report) {
(Some(maximum), Some(quality_report)) =>
for finding in quality_report.findings_value {
if severity_rank(finding.severity_value) > severity_rank(maximum) {
match finding.point_index_value {
Some(index) if index >= 0 && index < excluded.length() =>
excluded[index] = true
_ => ()
}
match finding.segment_index_value {
Some(index) if index + 1 >= 0 && index + 1 < excluded.length() =>
excluded[index + 1] = true
_ => ()
}
}
}
_ => ()
}
let points : Array[TrackPoint] = []
for index = 0; index < track.points_value.length(); index = index + 1 {
let point = track.points_value[index]
if !excluded[index] && point_matches_filter(point, filter) {
points.push(point)
}
}
Track::from_points(points)
}
///|
pub fn TrackFinding::kind(self : TrackFinding) -> TrackFindingKind {
self.kind_value
}
///|
pub fn TrackFinding::severity(self : TrackFinding) -> Severity {
self.severity_value
}
///|
pub fn TrackFinding::point_index(self : TrackFinding) -> Int? {
self.point_index_value
}
///|
pub fn TrackFinding::segment_index(self : TrackFinding) -> Int? {
self.segment_index_value
}
///|
pub fn TrackFinding::diagnostic(self : TrackFinding) -> Diagnostic {
self.diagnostic_value
}
///|
pub fn TrackQualityReport::findings(
self : TrackQualityReport,
) -> Array[TrackFinding] {
self.findings_value.copy()
}