///|
pub fn nearest_point_on_route(
  query : Point,
  route : ArrayView[Point],
) -> Result[NearestPoint, RouteError] {
  match validate_point(query, 0) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  match require_points(route) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  if route.length() == 1 {
    let distance = match haversine_meters(query, route[0]) {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    return Ok(NearestPoint(route[0], distance, 0.0, 0, 0.0))
  }
  let mut best = NearestPoint(route[0], 999999999999.0, 0.0, 0, 0.0)
  let mut walked = 0.0
  for i in 1.. value
      Err(err) => return Err(err)
    }
    if candidate.distance_to_route_m < best.distance_to_route_m {
      best = candidate
    }
    match haversine_meters(route[i - 1], route[i]) {
      Ok(value) => walked = walked + value
      Err(err) => return Err(err)
    }
  }
  Ok(best)
}

///|
pub fn distance_to_route_meters(
  query : Point,
  route : ArrayView[Point],
) -> Result[Double, RouteError] {
  match nearest_point_on_route(query, route) {
    Ok(value) => Ok(value.distance_to_route_m)
    Err(err) => Err(err)
  }
}

///|
pub fn route_distance_along_for_point(
  query : Point,
  route : ArrayView[Point],
) -> Result[Double, RouteError] {
  match nearest_point_on_route(query, route) {
    Ok(value) => Ok(value.distance_along_route_m)
    Err(err) => Err(err)
  }
}

///|
pub fn route_progress_fraction(
  query : Point,
  route : ArrayView[Point],
) -> Result[Double, RouteError] {
  let nearest = match nearest_point_on_route(query, route) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let total = match route_distance_meters(route) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  if total == 0.0 {
    Ok(0.0)
  } else {
    Ok(nearest.distance_along_route_m / total)
  }
}

///|
pub fn is_point_near_route(
  query : Point,
  route : ArrayView[Point],
  tolerance_m : Double,
) -> Result[Bool, RouteError] {
  match validate_distance(tolerance_m) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let distance = match distance_to_route_meters(query, route) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  Ok(distance <= tolerance_m)
}

///|
pub fn snap_point_to_route(
  query : Point,
  route : ArrayView[Point],
) -> Result[Point, RouteError] {
  match nearest_point_on_route(query, route) {
    Ok(value) => Ok(value.point)
    Err(err) => Err(err)
  }
}

///|
pub fn snap_points_to_route(
  queries : ArrayView[Point],
  route : ArrayView[Point],
) -> Result[Array[NearestPoint], RouteError] {
  match validate_points(queries) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  match require_points(route) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let out = Array::new(capacity=queries.length())
  for query in queries {
    match nearest_point_on_route(query, route) {
      Ok(value) => out.push(value)
      Err(err) => return Err(err)
    }
  }
  Ok(out)
}

///|
pub fn points_within_route_buffer(
  queries : ArrayView[Point],
  route : ArrayView[Point],
  tolerance_m : Double,
) -> Result[Array[Point], RouteError] {
  match validate_distance(tolerance_m) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  match validate_points(queries) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let out = Array::new()
  for query in queries {
    match is_point_near_route(query, route, tolerance_m) {
      Ok(true) => out.push(query)
      Ok(false) => ()
      Err(err) => return Err(err)
    }
  }
  Ok(out)
}

///|
pub fn route_station_for_fraction(
  route : ArrayView[Point],
  fraction : Double,
) -> Result[RouteSample, RouteError] {
  match validate_fraction(fraction) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let total = match route_distance_meters(route) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  point_at_distance(route, total * fraction)
}

///|
pub fn route_station_points(
  route : ArrayView[Point],
  fractions : ArrayView[Double],
) -> Result[Array[RouteSample], RouteError] {
  let out = Array::new(capacity=fractions.length())
  for fraction in fractions {
    match route_station_for_fraction(route, fraction) {
      Ok(value) => out.push(value)
      Err(err) => return Err(err)
    }
  }
  Ok(out)
}

///|
pub fn cross_track_distance_meters(
  query : Point,
  start : Point,
  finish : Point,
) -> Result[Double, RouteError] {
  match validate_point(query, 0) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let d13 = match haversine_meters(start, query) {
    Ok(value) => value / EARTH_RADIUS_M
    Err(err) => return Err(err)
  }
  let bearing13 = match initial_bearing_degrees(start, query) {
    Ok(value) => deg_to_rad(value)
    Err(err) => return Err(err)
  }
  let bearing12 = match initial_bearing_degrees(start, finish) {
    Ok(value) => deg_to_rad(value)
    Err(err) => return Err(err)
  }
  Ok(
    @math.asin(@math.sin(d13) * @math.sin(bearing13 - bearing12)) *
    EARTH_RADIUS_M,
  )
}

///|
pub fn along_track_distance_meters(
  query : Point,
  start : Point,
  finish : Point,
) -> Result[Double, RouteError] {
  let cross_track = match cross_track_distance_meters(query, start, finish) {
    Ok(value) => value / EARTH_RADIUS_M
    Err(err) => return Err(err)
  }
  let d13 = match haversine_meters(start, query) {
    Ok(value) => value / EARTH_RADIUS_M
    Err(err) => return Err(err)
  }
  let value = @math.acos(@math.cos(d13) / @math.cos(cross_track)) *
    EARTH_RADIUS_M
  if value.is_nan() || value.is_inf() {
    Ok(0.0)
  } else {
    Ok(value)
  }
}

///|
pub fn segment_fraction_for_projection(
  query : Point,
  start : Point,
  finish : Point,
) -> Result[Double, RouteError] {
  match validate_point(query, 0) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  match validate_point(start, 1) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  match validate_point(finish, 2) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let ref_lat = (query.lat + start.lat + finish.lat) / 3.0
  let (px, py) = project_for_distance(query, ref_lat)
  let (ax, ay) = project_for_distance(start, ref_lat)
  let (bx, by) = project_for_distance(finish, ref_lat)
  let dx = bx - ax
  let dy = by - ay
  if dx == 0.0 && dy == 0.0 {
    Ok(0.0)
  } else {
    Ok(
      clamp_double(
        ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy),
        0.0,
        1.0,
      ),
    )
  }
}

///|
fn nearest_point_on_segment(
  query : Point,
  start : Point,
  finish : Point,
  segment_index : Int,
  distance_before_segment_m : Double,
) -> Result[NearestPoint, RouteError] {
  let fraction = match segment_fraction_for_projection(query, start, finish) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let snapped = interpolate_point_unchecked(start, finish, fraction)
  let off_distance = match haversine_meters(query, snapped) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let segment_m = match haversine_meters(start, finish) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  Ok(
    NearestPoint(
      snapped,
      off_distance,
      distance_before_segment_m + segment_m * fraction,
      segment_index,
      fraction,
    ),
  )
}