///|
/// WGS84 route point. Latitude and longitude are stored in degrees.
pub struct Point {
lat : Double
lon : Double
} derive(Eq)
///|
/// Latitude/longitude bounding box in WGS84 degrees.
pub struct BBox {
min_lat : Double
min_lon : Double
max_lat : Double
max_lon : Double
}
///|
/// Slippy-map tile coordinate in the Web Mercator tiling scheme.
pub struct Tile {
z : Int
x : Int
y : Int
} derive(Eq)
///|
/// Compact summary for one route and one tile zoom level.
pub struct RouteSummary {
point_count : Int
simplified_point_count : Int
distance_m : Double
bbox : BBox
encoded_polyline : String
tiles : Array[Tile]
zoom : Int
}
///|
/// Per-segment route metrics, useful for route tables and diagnostics.
pub struct SegmentMetric {
index : Int
start : Point
finish : Point
distance_m : Double
bearing_deg : Double
}
///|
/// Distribution summary for route segment lengths.
pub struct SegmentSpread {
segment_count : Int
total_distance_m : Double
shortest_segment_m : Double
longest_segment_m : Double
average_segment_m : Double
standard_deviation_m : Double
coefficient_of_variation : Double
longest_segment_index : Int
}
///|
/// Cumulative distance marker at a concrete point on a route.
pub struct RouteMarker {
index : Int
point : Point
distance_m : Double
}
///|
/// Sample generated from a route at a requested distance.
pub struct RouteSample {
point : Point
distance_m : Double
segment_index : Int
fraction : Double
}
///|
/// Result of cutting a route between two distances.
pub struct RouteSlice {
start_m : Double
end_m : Double
distance_m : Double
points : Array[Point]
}
///|
/// Nearest-point projection result for snapping a location onto a route.
pub struct NearestPoint {
point : Point
distance_to_route_m : Double
distance_along_route_m : Double
segment_index : Int
fraction : Double
}
///|
/// Aggregate route statistics for dashboards and validation records.
pub struct RouteStats {
point_count : Int
segment_count : Int
distance_m : Double
direct_distance_m : Double
sinuosity : Double
min_segment_m : Double
max_segment_m : Double
average_segment_m : Double
bbox : BBox
center : Point
}
///|
/// Geographic bounds of one Web Mercator tile.
pub struct TileBounds {
tile : Tile
west : Double
south : Double
east : Double
north : Double
}
///|
/// Inclusive tile coordinate range for a bounding box at one zoom level.
pub struct TileRange {
z : Int
min_x : Int
min_y : Int
max_x : Int
max_y : Int
}
///|
/// Public error type shared by route codecs, metrics, simplification, and tiles.
pub(all) enum RouteError {
EmptyRoute
InvalidCoordinate(index~ : Int, lat~ : Double, lon~ : Double)
InvalidPrecision(precision~ : Int)
InvalidZoom(zoom~ : Int)
InvalidTolerance(tolerance_m~ : Double)
InvalidDistance(distance_m~ : Double)
InvalidFraction(fraction~ : Double)
InvalidSegmentLength(length_m~ : Double)
InvalidSampleCount(count~ : Int)
InvalidTileCoordinate(tile~ : Tile)
MalformedPolyline(pos~ : Int, reason~ : String)
} derive(Eq)
///|
pub fn Point::Point(lat : Double, lon : Double) -> Point {
{ lat, lon }
}
///|
pub fn BBox::BBox(
min_lat : Double,
min_lon : Double,
max_lat : Double,
max_lon : Double,
) -> BBox {
{ min_lat, min_lon, max_lat, max_lon }
}
///|
pub fn Tile::Tile(z : Int, x : Int, y : Int) -> Tile {
{ z, x, y }
}
///|
pub fn SegmentMetric::SegmentMetric(
index : Int,
start : Point,
finish : Point,
distance_m : Double,
bearing_deg : Double,
) -> SegmentMetric {
{ index, start, finish, distance_m, bearing_deg }
}
///|
pub fn SegmentSpread::SegmentSpread(
segment_count : Int,
total_distance_m : Double,
shortest_segment_m : Double,
longest_segment_m : Double,
average_segment_m : Double,
standard_deviation_m : Double,
coefficient_of_variation : Double,
longest_segment_index : Int,
) -> SegmentSpread {
{
segment_count,
total_distance_m,
shortest_segment_m,
longest_segment_m,
average_segment_m,
standard_deviation_m,
coefficient_of_variation,
longest_segment_index,
}
}
///|
pub fn RouteMarker::RouteMarker(
index : Int,
point : Point,
distance_m : Double,
) -> RouteMarker {
{ index, point, distance_m }
}
///|
pub fn RouteSample::RouteSample(
point : Point,
distance_m : Double,
segment_index : Int,
fraction : Double,
) -> RouteSample {
{ point, distance_m, segment_index, fraction }
}
///|
pub fn RouteSlice::RouteSlice(
start_m : Double,
end_m : Double,
distance_m : Double,
points : Array[Point],
) -> RouteSlice {
{ start_m, end_m, distance_m, points }
}
///|
pub fn NearestPoint::NearestPoint(
point : Point,
distance_to_route_m : Double,
distance_along_route_m : Double,
segment_index : Int,
fraction : Double,
) -> NearestPoint {
{
point,
distance_to_route_m,
distance_along_route_m,
segment_index,
fraction,
}
}
///|
pub fn RouteStats::RouteStats(
point_count : Int,
segment_count : Int,
distance_m : Double,
direct_distance_m : Double,
sinuosity : Double,
min_segment_m : Double,
max_segment_m : Double,
average_segment_m : Double,
bbox : BBox,
center : Point,
) -> RouteStats {
{
point_count,
segment_count,
distance_m,
direct_distance_m,
sinuosity,
min_segment_m,
max_segment_m,
average_segment_m,
bbox,
center,
}
}
///|
pub fn TileBounds::TileBounds(
tile : Tile,
west : Double,
south : Double,
east : Double,
north : Double,
) -> TileBounds {
{ tile, west, south, east, north }
}
///|
pub fn TileRange::TileRange(
z : Int,
min_x : Int,
min_y : Int,
max_x : Int,
max_y : Int,
) -> TileRange {
{ z, min_x, min_y, max_x, max_y }
}
///|
pub fn Point::is_valid(self : Point) -> Bool {
is_valid_lat(self.lat) && is_valid_lon(self.lon)
}
///|
pub fn Point::to_string(self : Point) -> String {
"(\{self.lat}, \{self.lon})"
}
///|
pub fn BBox::to_string(self : BBox) -> String {
"\{self.min_lat},\{self.min_lon}..\{self.max_lat},\{self.max_lon}"
}
///|
pub fn Tile::key(self : Tile) -> String {
"\{self.z}/\{self.x}/\{self.y}"
}
///|
pub fn Tile::to_string(self : Tile) -> String {
self.key()
}
///|
pub fn SegmentMetric::to_string(self : SegmentMetric) -> String {
"\{self.index}: \{round_meters(self.distance_m)}m @ \{round_meters(self.bearing_deg)}deg"
}
///|
pub fn SegmentSpread::to_string(self : SegmentSpread) -> String {
"segments=\{self.segment_count}, avg=\{round_meters(self.average_segment_m)}m, stddev=\{round_meters(self.standard_deviation_m)}m, cv=\{self.coefficient_of_variation}"
}
///|
pub fn RouteMarker::to_string(self : RouteMarker) -> String {
"\{self.index}@\{round_meters(self.distance_m)}m \{self.point.to_string()}"
}
///|
pub fn RouteSample::to_string(self : RouteSample) -> String {
"\{round_meters(self.distance_m)}m on segment \{self.segment_index}:\{self.fraction} \{self.point.to_string()}"
}
///|
pub fn RouteSlice::to_string(self : RouteSlice) -> String {
"\{round_meters(self.start_m)}..\{round_meters(self.end_m)}m, points=\{self.points.length()}"
}
///|
pub fn NearestPoint::to_string(self : NearestPoint) -> String {
"segment=\{self.segment_index}, along=\{round_meters(self.distance_along_route_m)}m, off=\{round_meters(self.distance_to_route_m)}m"
}
///|
pub fn TileRange::to_string(self : TileRange) -> String {
"\{self.z}/\{self.min_x},\{self.min_y}..\{self.max_x},\{self.max_y}"
}
///|
pub fn TileBounds::to_bbox(self : TileBounds) -> BBox {
BBox(self.south, self.west, self.north, self.east)
}
///|
pub fn RouteError::message(self : RouteError) -> String {
match self {
EmptyRoute => "route is empty"
InvalidCoordinate(index~, lat~, lon~) =>
"invalid coordinate at index \{index}: lat=\{lat}, lon=\{lon}"
InvalidPrecision(precision~) =>
"polyline precision must be between 0 and 7, got \{precision}"
InvalidZoom(zoom~) => "tile zoom must be between 0 and 22, got \{zoom}"
InvalidTolerance(tolerance_m~) =>
"simplify tolerance must be finite and non-negative, got \{tolerance_m}"
InvalidDistance(distance_m~) =>
"distance must be finite and non-negative, got \{distance_m}"
InvalidFraction(fraction~) =>
"fraction must be finite and between 0 and 1, got \{fraction}"
InvalidSegmentLength(length_m~) =>
"segment length must be finite and positive, got \{length_m}"
InvalidSampleCount(count~) => "sample count must be positive, got \{count}"
InvalidTileCoordinate(tile~) =>
"tile coordinate is outside zoom range: \{tile.to_string()}"
MalformedPolyline(pos~, reason~) =>
"malformed encoded polyline at offset \{pos}: \{reason}"
}
}
///|
pub fn round_meters(value : Double) -> Int {
value.round().to_int()
}
///|
pub fn meters_to_kilometers(value : Double) -> Double {
value / 1000.0
}
///|
pub fn kilometers_to_meters(value : Double) -> Double {
value * 1000.0
}
///|
pub fn meters_to_miles(value : Double) -> Double {
value / 1609.344
}
///|
pub fn miles_to_meters(value : Double) -> Double {
value * 1609.344
}
///|
pub fn degrees_to_radians(value : Double) -> Double {
deg_to_rad(value)
}
///|
pub fn radians_to_degrees(value : Double) -> Double {
value * 180.0 / @math.PI
}
///|
pub fn normalize_bearing_degrees(value : Double) -> Double {
let mut out = value
while out < 0.0 {
out = out + 360.0
}
while out >= 360.0 {
out = out - 360.0
}
out
}
///|
pub fn bearing_delta_degrees(from : Double, to : Double) -> Double {
let raw = normalize_bearing_degrees(to) - normalize_bearing_degrees(from)
if raw > 180.0 {
raw - 360.0
} else if raw < -180.0 {
raw + 360.0
} else {
raw
}
}
///|
pub fn compass_direction(bearing_deg : Double) -> String {
let bearing = normalize_bearing_degrees(bearing_deg)
if bearing < 22.5 || bearing >= 337.5 {
"N"
} else if bearing < 67.5 {
"NE"
} else if bearing < 112.5 {
"E"
} else if bearing < 157.5 {
"SE"
} else if bearing < 202.5 {
"S"
} else if bearing < 247.5 {
"SW"
} else if bearing < 292.5 {
"W"
} else {
"NW"
}
}
///|
fn is_valid_lat(value : Double) -> Bool {
!value.is_nan() && !value.is_inf() && value >= -90.0 && value <= 90.0
}
///|
fn is_valid_lon(value : Double) -> Bool {
!value.is_nan() && !value.is_inf() && value >= -180.0 && value <= 180.0
}
///|
fn validate_point(point : Point, index : Int) -> Result[Unit, RouteError] {
if point.is_valid() {
Ok(())
} else {
Err(InvalidCoordinate(index~, lat=point.lat, lon=point.lon))
}
}
///|
fn validate_points(points : ArrayView[Point]) -> Result[Unit, RouteError] {
for i, point in points {
match validate_point(point, i) {
Ok(_) => ()
Err(err) => return Err(err)
}
}
Ok(())
}
///|
fn require_points(points : ArrayView[Point]) -> Result[Unit, RouteError] {
if points.length() == 0 {
Err(EmptyRoute)
} else {
validate_points(points)
}
}
///|
fn validate_precision(precision : Int) -> Result[Unit, RouteError] {
if precision >= 0 && precision <= 7 {
Ok(())
} else {
Err(InvalidPrecision(precision~))
}
}
///|
fn validate_zoom(zoom : Int) -> Result[Unit, RouteError] {
if zoom >= 0 && zoom <= 22 {
Ok(())
} else {
Err(InvalidZoom(zoom~))
}
}
///|
fn validate_distance(distance_m : Double) -> Result[Unit, RouteError] {
if is_finite_non_negative(distance_m) {
Ok(())
} else {
Err(InvalidDistance(distance_m~))
}
}
///|
fn validate_positive_distance(distance_m : Double) -> Result[Unit, RouteError] {
if !distance_m.is_nan() && !distance_m.is_inf() && distance_m > 0.0 {
Ok(())
} else {
Err(InvalidSegmentLength(length_m=distance_m))
}
}
///|
fn validate_fraction(fraction : Double) -> Result[Unit, RouteError] {
if !fraction.is_nan() &&
!fraction.is_inf() &&
fraction >= 0.0 &&
fraction <= 1.0 {
Ok(())
} else {
Err(InvalidFraction(fraction~))
}
}
///|
fn validate_sample_count(count : Int) -> Result[Unit, RouteError] {
if count > 0 {
Ok(())
} else {
Err(InvalidSampleCount(count~))
}
}
///|
fn pow10_int(exp : Int) -> Int {
let mut out = 1
for _ in 0.. Double {
value * @math.PI / 180.0
}
///|
fn rad_to_deg(value : Double) -> Double {
value * 180.0 / @math.PI
}
///|
fn is_finite_non_negative(value : Double) -> Bool {
!value.is_nan() && !value.is_inf() && value >= 0.0
}
///|
fn abs_double(value : Double) -> Double {
if value < 0.0 {
-value
} else {
value
}
}
///|
fn min_double(left : Double, right : Double) -> Double {
if left < right {
left
} else {
right
}
}
///|
fn max_double(left : Double, right : Double) -> Double {
if left > right {
left
} else {
right
}
}
///|
fn clamp_double(value : Double, min : Double, max : Double) -> Double {
if value < min {
min
} else if value > max {
max
} else {
value
}
}
///|
fn lerp_double(start : Double, finish : Double, fraction : Double) -> Double {
start + (finish - start) * fraction
}
///|
fn normalize_longitude_delta(delta : Double) -> Double {
let mut out = delta
while out > 180.0 {
out = out - 360.0
}
while out < -180.0 {
out = out + 360.0
}
out
}
///|
fn interpolate_point_unchecked(
a : Point,
b : Point,
fraction : Double,
) -> Point {
let lat = lerp_double(a.lat, b.lat, fraction)
let lon_delta = normalize_longitude_delta(b.lon - a.lon)
let mut lon = a.lon + lon_delta * fraction
while lon > 180.0 {
lon = lon - 360.0
}
while lon < -180.0 {
lon = lon + 360.0
}
Point(lat, lon)
}
///|
fn points_equal(a : Point, b : Point) -> Bool {
a.lat == b.lat && a.lon == b.lon
}
///|
fn tile_contains(tiles : Array[Tile], tile : Tile) -> Bool {
for item in tiles {
if item == tile {
return true
}
}
false
}
///|
fn push_unique_tile(tiles : Array[Tile], tile : Tile) -> Unit {
if !tile_contains(tiles, tile) {
tiles.push(tile)
}
}
///|
fn max_int(left : Int, right : Int) -> Int {
if left > right {
left
} else {
right
}
}
///|
fn min_int(left : Int, right : Int) -> Int {
if left < right {
left
} else {
right
}
}