///|
pub struct RouteIssue {
index : Int
code : String
severity : Int
detail : String
}
///|
pub struct RouteQualityReport {
point_count : Int
segment_count : Int
duplicate_points : Int
stationary_segments : Int
long_segments : Int
sharp_turns : Int
self_intersections : Int
score : Int
issues : Array[RouteIssue]
}
///|
pub fn RouteIssue::RouteIssue(
index : Int,
code : String,
severity : Int,
detail : String,
) -> RouteIssue {
{ index, code, severity, detail }
}
///|
pub fn RouteIssue::to_string(self : RouteIssue) -> String {
"\{self.code}@\{self.index}: \{self.detail}"
}
///|
pub fn RouteQualityReport::report(self : RouteQualityReport) -> String {
[
"point_count: \{self.point_count}",
"segment_count: \{self.segment_count}",
"duplicate_points: \{self.duplicate_points}",
"stationary_segments: \{self.stationary_segments}",
"long_segments: \{self.long_segments}",
"sharp_turns: \{self.sharp_turns}",
"self_intersections: \{self.self_intersections}",
"score: \{self.score}",
"issue_count: \{self.issues.length()}",
].join("\n")
}
///|
pub fn duplicate_point_indices(
points : ArrayView[Point],
) -> Result[Array[Int], RouteError] {
match require_points(points) {
Ok(_) => ()
Err(err) => return Err(err)
}
let out = Array::new()
if points.length() < 2 {
return Ok(out)
}
for i in 1.. Result[Array[Int], RouteError] {
match validate_distance(threshold_m) {
Ok(_) => ()
Err(err) => return Err(err)
}
match require_points(points) {
Ok(_) => ()
Err(err) => return Err(err)
}
let out = Array::new()
if points.length() < 2 {
return Ok(out)
}
for i in 1.. value
Err(err) => return Err(err)
}
if distance <= threshold_m {
out.push(i - 1)
}
}
Ok(out)
}
///|
pub fn long_segment_indices(
points : ArrayView[Point],
max_segment_m : Double,
) -> Result[Array[Int], RouteError] {
match validate_positive_distance(max_segment_m) {
Ok(_) => ()
Err(err) => return Err(err)
}
match require_points(points) {
Ok(_) => ()
Err(err) => return Err(err)
}
let out = Array::new()
if points.length() < 2 {
return Ok(out)
}
for i in 1.. value
Err(err) => return Err(err)
}
if distance > max_segment_m {
out.push(i - 1)
}
}
Ok(out)
}
///|
pub fn sharp_turn_indices(
points : ArrayView[Point],
threshold_deg? : Double = 100.0,
) -> Result[Array[Int], RouteError] {
if threshold_deg.is_nan() || threshold_deg.is_inf() || threshold_deg < 0.0 {
return Err(InvalidFraction(fraction=threshold_deg))
}
let turns = match route_turn_angles(points) {
Ok(value) => value
Err(err) => return Err(err)
}
let out = Array::new()
for i, turn in turns {
if abs_double(turn) >= threshold_deg {
out.push(i + 1)
}
}
Ok(out)
}
///|
pub fn self_intersection_count(
points : ArrayView[Point],
) -> Result[Int, RouteError] {
match require_points(points) {
Ok(_) => ()
Err(err) => return Err(err)
}
if points.length() < 4 {
return Ok(0)
}
let mut count = 0
for a in 1.. Result[Array[RouteIssue], RouteError] {
let issues = Array::new()
let duplicates = match duplicate_point_indices(points) {
Ok(value) => value
Err(err) => return Err(err)
}
for index in duplicates {
issues.push(
RouteIssue(
index, "duplicate_point", 2, "same coordinate as previous point",
),
)
}
let stationary = match
stationary_segment_indices(points, threshold_m=stationary_threshold_m) {
Ok(value) => value
Err(err) => return Err(err)
}
for index in stationary {
issues.push(
RouteIssue(
index, "stationary_segment", 1, "segment is shorter than threshold",
),
)
}
let long_segments = match long_segment_indices(points, max_segment_m) {
Ok(value) => value
Err(err) => return Err(err)
}
for index in long_segments {
issues.push(
RouteIssue(index, "long_segment", 3, "segment is longer than expected"),
)
}
let sharp_turns = match
sharp_turn_indices(points, threshold_deg=sharp_turn_deg) {
Ok(value) => value
Err(err) => return Err(err)
}
for index in sharp_turns {
issues.push(
RouteIssue(index, "sharp_turn", 1, "bearing change exceeds threshold"),
)
}
let intersections = match self_intersection_count(points) {
Ok(value) => value
Err(err) => return Err(err)
}
if intersections > 0 {
issues.push(
RouteIssue(
0,
"self_intersection",
2,
"\{intersections} crossing segments detected",
),
)
}
Ok(issues)
}
///|
pub fn route_quality_report(
points : ArrayView[Point],
stationary_threshold_m? : Double = 1.0,
max_segment_m? : Double = 2000.0,
sharp_turn_deg? : Double = 100.0,
) -> Result[RouteQualityReport, RouteError] {
match require_points(points) {
Ok(_) => ()
Err(err) => return Err(err)
}
let duplicates = match duplicate_point_indices(points) {
Ok(value) => value
Err(err) => return Err(err)
}
let stationary = match
stationary_segment_indices(points, threshold_m=stationary_threshold_m) {
Ok(value) => value
Err(err) => return Err(err)
}
let long_segments = match long_segment_indices(points, max_segment_m) {
Ok(value) => value
Err(err) => return Err(err)
}
let sharp_turns = match
sharp_turn_indices(points, threshold_deg=sharp_turn_deg) {
Ok(value) => value
Err(err) => return Err(err)
}
let intersections = match self_intersection_count(points) {
Ok(value) => value
Err(err) => return Err(err)
}
let issues = match
route_quality_issues(
points,
stationary_threshold_m~,
max_segment_m~,
sharp_turn_deg~,
) {
Ok(value) => value
Err(err) => return Err(err)
}
let score = route_quality_score(issues)
Ok({
point_count: points.length(),
segment_count: max_int(points.length() - 1, 0),
duplicate_points: duplicates.length(),
stationary_segments: stationary.length(),
long_segments: long_segments.length(),
sharp_turns: sharp_turns.length(),
self_intersections: intersections,
score,
issues,
})
}
///|
pub fn remove_consecutive_duplicates(
points : ArrayView[Point],
) -> Result[Array[Point], RouteError] {
match require_points(points) {
Ok(_) => ()
Err(err) => return Err(err)
}
let out = Array::new()
out.push(points[0])
for i in 1.. Result[Array[Point], RouteError] {
match validate_distance(threshold_m) {
Ok(_) => ()
Err(err) => return Err(err)
}
match require_points(points) {
Ok(_) => ()
Err(err) => return Err(err)
}
let out = Array::new()
out.push(points[0])
for i in 1.. value
Err(err) => return Err(err)
}
if distance > threshold_m || i == points.length() - 1 {
out.push(points[i])
}
}
Ok(out)
}
///|
pub fn smooth_route_window(
points : ArrayView[Point],
radius : Int,
) -> Result[Array[Point], RouteError] {
match require_points(points) {
Ok(_) => ()
Err(err) => return Err(err)
}
if radius < 0 {
return Err(InvalidSampleCount(count=radius))
}
if radius == 0 || points.length() <= 2 {
return Ok(points.to_owned())
}
let out = Array::new(capacity=points.length())
for i in 0.. Int {
let mut penalty = 0
for issue in issues {
penalty = penalty + issue.severity * 8
}
let score = 100 - penalty
if score < 0 {
0
} else {
score
}
}
///|
fn segments_intersect(a1 : Point, a2 : Point, b1 : Point, b2 : Point) -> Bool {
let d1 = direction(a1, a2, b1)
let d2 = direction(a1, a2, b2)
let d3 = direction(b1, b2, a1)
let d4 = direction(b1, b2, a2)
if ((d1 > 0.0 && d2 < 0.0) || (d1 < 0.0 && d2 > 0.0)) &&
((d3 > 0.0 && d4 < 0.0) || (d3 < 0.0 && d4 > 0.0)) {
true
} else if d1 == 0.0 && on_segment(a1, a2, b1) {
true
} else if d2 == 0.0 && on_segment(a1, a2, b2) {
true
} else if d3 == 0.0 && on_segment(b1, b2, a1) {
true
} else if d4 == 0.0 && on_segment(b1, b2, a2) {
true
} else {
false
}
}
///|
fn direction(a : Point, b : Point, c : Point) -> Double {
(c.lon - a.lon) * (b.lat - a.lat) - (b.lon - a.lon) * (c.lat - a.lat)
}
///|
fn on_segment(a : Point, b : Point, c : Point) -> Bool {
c.lon >= min_double(a.lon, b.lon) &&
c.lon <= max_double(a.lon, b.lon) &&
c.lat >= min_double(a.lat, b.lat) &&
c.lat <= max_double(a.lat, b.lat)
}