blaise 0.1.4

A fast, local-first engine for GTFS transit data. Handles routing, fuzzy search, and geospatial queries without relying on external APIs.
Documentation
use crate::repository::Cell;
use serde::{Deserialize, Serialize};
use std::{
    cmp,
    fmt::Display,
    iter::Sum,
    ops::{Add, Div, Mul, Sub},
    str::FromStr,
};
use thiserror::Error;

pub const AVERAGE_STOP_DISTANCE: Distance = Distance::from_meters(500.0);
pub(crate) const LONGITUDE_DISTANCE: Distance = Distance::from_meters(111_320.0);
pub(crate) const LATITUDE_DISTANCE: Distance = Distance::from_meters(110_540.0);

#[derive(Debug, Clone, Copy, Default)]
pub struct Distance(f32);

impl PartialEq for Distance {
    fn eq(&self, other: &Self) -> bool {
        self.0 == other.0
    }
}

impl PartialOrd for Distance {
    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
        self.0.partial_cmp(&other.0)
    }
}

impl Add for Distance {
    type Output = Self;
    fn add(self, rhs: Self) -> Self::Output {
        Self(self.0 + rhs.0)
    }
}

impl Sub for Distance {
    type Output = Self;

    fn sub(self, rhs: Self) -> Self::Output {
        Self(self.0 - rhs.0)
    }
}

impl Mul for Distance {
    type Output = Self;
    fn mul(self, rhs: Self) -> Self::Output {
        Self(self.0 * rhs.0)
    }
}

impl Div for Distance {
    type Output = Self;
    fn div(self, rhs: Self) -> Self::Output {
        Self(self.0 / rhs.0)
    }
}

impl From<f32> for Distance {
    fn from(value: f32) -> Self {
        Distance(value)
    }
}

impl Distance {
    pub const fn from_meters(distance: f32) -> Self {
        Self(distance)
    }

    pub const fn from_kilometers(distance: f32) -> Self {
        Self(distance * 1000.0)
    }

    pub const fn as_meters(&self) -> f32 {
        self.0
    }

    pub const fn as_kilometers(&self) -> f32 {
        self.0 / 1000.0
    }
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Coordinate {
    pub latitude: f32,
    pub longitude: f32,
}

impl Sum for Coordinate {
    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
        let mut count: usize = 0;
        let mut lat: f32 = 0.0;
        let mut lon: f32 = 0.0;
        iter.for_each(|coordinate| {
            count += 1;
            lat += coordinate.latitude;
            lon += coordinate.longitude;
        });
        let count = count as f32;
        Self {
            latitude: lat / count,
            longitude: lon / count,
        }
    }
}

impl From<Coordinate> for (f32, f32) {
    fn from(value: Coordinate) -> Self {
        (value.latitude, value.longitude)
    }
}

impl From<(f32, f32)> for Coordinate {
    fn from(value: (f32, f32)) -> Self {
        Self {
            latitude: value.0,
            longitude: value.1,
        }
    }
}

impl Display for Coordinate {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_fmt(format_args!("{}, {}", self.latitude, self.longitude))
    }
}

#[derive(Error, Debug)]
pub enum ParseCoordinateError {
    #[error("Invalid latitude")]
    InvalidLatitude,
    #[error("Invalid longitude")]
    InvalidLongitude,
    #[error("Invalid format")]
    InvalidFormat,
}

impl FromStr for Coordinate {
    type Err = ParseCoordinateError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if !s.contains(',') {
            return Err(ParseCoordinateError::InvalidFormat);
        }
        let s: String = s.split_whitespace().collect();
        let split: Vec<_> = s.split(',').collect();
        let latitude: f32 = split
            .first()
            .ok_or(ParseCoordinateError::InvalidLatitude)?
            .parse()
            .map_err(|_| ParseCoordinateError::InvalidLatitude)?;
        let longitude: f32 = split
            .last()
            .ok_or(ParseCoordinateError::InvalidLongitude)?
            .parse()
            .map_err(|_| ParseCoordinateError::InvalidLongitude)?;
        Ok(Coordinate {
            latitude,
            longitude,
        })
    }
}

impl Coordinate {
    pub fn new(latitude: f32, longitude: f32) -> Self {
        Self {
            latitude,
            longitude,
        }
    }

    pub fn euclidean_distance(&self, coord: &Self) -> Distance {
        const R: f32 = 6371.0;
        let dist_lat = f32::to_radians(coord.latitude - self.latitude);
        let dist_lon = f32::to_radians(coord.longitude - self.longitude);
        let a = f32::powi(f32::sin(dist_lat / 2.0), 2)
            + f32::cos(f32::to_radians(self.latitude))
                * f32::cos(f32::to_radians(coord.latitude))
                * f32::sin(dist_lon / 2.0)
                * f32::sin(dist_lon / 2.0);
        let c = 2.0 * f32::atan2(f32::sqrt(a), f32::sqrt(1.0 - a));
        Distance::from_kilometers(R * c)
    }

    pub fn network_distance(&self, coord: &Self) -> Distance {
        const CIRCUITY_FACTOR: f32 = 1.3;
        Distance::from_meters(self.euclidean_distance(coord).as_meters() * CIRCUITY_FACTOR)
    }

    pub fn to_cell(&self) -> Cell {
        let x = (self.longitude * LONGITUDE_DISTANCE.as_meters()
            / AVERAGE_STOP_DISTANCE.as_meters()) as i32;
        let y = (self.latitude * LATITUDE_DISTANCE.as_meters() / AVERAGE_STOP_DISTANCE.as_meters())
            as i32;
        (x, y)
    }
}

#[test]
fn distance_test() {
    let coord_a = Coordinate {
        latitude: 48.858_01,
        longitude: 2.351_435,
    };

    let coord_b = Coordinate {
        latitude: 51.505_238,
        longitude: -0.124_954_075,
    };
    let d = coord_a.euclidean_distance(&coord_b);
    assert!((d.as_kilometers() - 343_000.0).abs() > 500.0);
}

#[test]
fn distance_eq_test() {
    let dist_a = Distance::from_meters(1000.0);
    let dist_b = Distance::from_kilometers(1.0);
    assert_eq!(dist_a, dist_b)
}

#[test]
fn distance_cmp_test() {
    let dist_a = Distance::from_meters(1000.0);
    let dist_b = Distance::from_kilometers(0.5);
    assert!(dist_a > dist_b)
}