use geo::{Coord, LineString, algorithm::simplify::Simplify};
use rstar::{AABB, RTreeObject};
use serde::{Deserialize, Serialize};
pub mod error;
pub use error::{OptionExt, Result, RouteMatchError};
pub mod union_find;
pub use union_find::UnionFind;
pub mod matching;
pub use matching::compare_routes;
pub mod grouping;
#[cfg(feature = "parallel")]
pub use grouping::{
group_incremental, group_signatures_parallel, group_signatures_parallel_with_matches,
};
pub use grouping::{group_signatures, group_signatures_with_matches, should_group_routes};
pub mod geo_utils;
pub mod algorithms;
pub mod engine;
pub use engine::{
ActivityData, ActivityStore, ModularEngineStats, ModularRouteEngine, RouteGrouper,
SignatureStore, SpatialIndex,
};
#[cfg(feature = "persistence")]
pub mod persistence;
#[cfg(feature = "persistence")]
pub use persistence::{
GroupSummary, PERSISTENT_ENGINE, PersistentEngineStats, PersistentRouteEngine,
SectionDetectionHandle, SectionSummary, with_persistent_engine,
};
pub mod sections;
pub use sections::{
DetectionStats,
FrequentSection,
MultiScaleSectionResult,
PotentialSection,
ScalePreset,
SectionConfig,
SectionMatch,
SectionPortion,
SplitResult,
detect_sections_from_tracks,
detect_sections_multiscale,
find_sections_in_route,
recalculate_section_polyline,
split_section_at_index,
split_section_at_point,
};
pub mod heatmap;
pub use heatmap::{
ActivityHeatmapData, CellQueryResult, HeatmapBounds, HeatmapCell, HeatmapConfig, HeatmapResult,
RouteRef, generate_heatmap, query_heatmap_cell,
};
#[cfg(feature = "http")]
pub mod http;
#[cfg(feature = "http")]
pub use http::{ActivityFetcher, ActivityMapResult, MapBounds};
#[cfg(feature = "ffi")]
pub mod ffi;
#[cfg(feature = "ffi")]
uniffi::setup_scaffolding!();
#[cfg(all(feature = "ffi", target_os = "android"))]
pub(crate) fn init_logging() {
use android_logger::Config;
use log::LevelFilter;
android_logger::init_once(
Config::default()
.with_max_level(LevelFilter::Debug)
.with_tag("RouteMatcherRust"),
);
}
#[cfg(all(feature = "ffi", not(target_os = "android")))]
pub(crate) fn init_logging() {
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "ffi", derive(uniffi::Record))]
pub struct GpsPoint {
pub latitude: f64,
pub longitude: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub elevation: Option<f64>,
}
impl GpsPoint {
pub fn new(latitude: f64, longitude: f64) -> Self {
Self {
latitude,
longitude,
elevation: None,
}
}
pub fn with_elevation(latitude: f64, longitude: f64, elevation: f64) -> Self {
Self {
latitude,
longitude,
elevation: Some(elevation),
}
}
pub fn is_valid(&self) -> bool {
self.latitude.is_finite()
&& self.longitude.is_finite()
&& self.latitude >= -90.0
&& self.latitude <= 90.0
&& self.longitude >= -180.0
&& self.longitude <= 180.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "ffi", derive(uniffi::Record))]
pub struct Bounds {
pub min_lat: f64,
pub max_lat: f64,
pub min_lng: f64,
pub max_lng: f64,
}
impl Bounds {
pub fn from_points(points: &[GpsPoint]) -> Option<Self> {
if points.is_empty() {
return None;
}
let mut min_lat = f64::MAX;
let mut max_lat = f64::MIN;
let mut min_lng = f64::MAX;
let mut max_lng = f64::MIN;
for p in points {
min_lat = min_lat.min(p.latitude);
max_lat = max_lat.max(p.latitude);
min_lng = min_lng.min(p.longitude);
max_lng = max_lng.max(p.longitude);
}
Some(Self {
min_lat,
max_lat,
min_lng,
max_lng,
})
}
pub fn center(&self) -> GpsPoint {
GpsPoint::new(
(self.min_lat + self.max_lat) / 2.0,
(self.min_lng + self.max_lng) / 2.0,
)
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "ffi", derive(uniffi::Record))]
pub struct RouteSignature {
pub activity_id: String,
pub points: Vec<GpsPoint>,
pub total_distance: f64,
pub start_point: GpsPoint,
pub end_point: GpsPoint,
pub bounds: Bounds,
pub center: GpsPoint,
}
impl RouteSignature {
pub fn from_points(
activity_id: &str,
points: &[GpsPoint],
config: &MatchConfig,
) -> Option<Self> {
if points.len() < 2 {
return None;
}
let coords: Vec<Coord> = points
.iter()
.filter(|p| p.is_valid())
.map(|p| Coord {
x: p.longitude,
y: p.latitude,
})
.collect();
if coords.len() < 2 {
return None;
}
let line = LineString::new(coords);
let simplified = line.simplify(&config.simplification_tolerance);
let final_coords: Vec<Coord> = if simplified.0.len() > config.max_simplified_points as usize
{
let step = simplified.0.len() as f64 / config.max_simplified_points as f64;
(0..config.max_simplified_points)
.map(|i| simplified.0[(i as f64 * step) as usize])
.collect()
} else {
simplified.0.clone()
};
if final_coords.len() < 2 {
return None;
}
let simplified_points: Vec<GpsPoint> = final_coords
.iter()
.map(|c| GpsPoint::new(c.y, c.x))
.collect();
let total_distance = calculate_route_distance(&simplified_points);
let bounds = Bounds::from_points(&simplified_points)?;
let center = bounds.center();
Some(Self {
activity_id: activity_id.to_string(),
start_point: simplified_points[0],
end_point: simplified_points[simplified_points.len() - 1],
points: simplified_points,
total_distance,
bounds,
center,
})
}
pub fn route_bounds(&self) -> RouteBounds {
RouteBounds {
activity_id: self.activity_id.clone(),
min_lat: self.bounds.min_lat,
max_lat: self.bounds.max_lat,
min_lng: self.bounds.min_lng,
max_lng: self.bounds.max_lng,
distance: self.total_distance,
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "ffi", derive(uniffi::Record))]
pub struct MatchResult {
pub activity_id_1: String,
pub activity_id_2: String,
pub match_percentage: f64,
pub direction: String,
pub amd: f64,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "ffi", derive(uniffi::Record))]
pub struct MatchConfig {
pub perfect_threshold: f64,
pub zero_threshold: f64,
pub min_match_percentage: f64,
pub min_route_distance: f64,
pub max_distance_diff_ratio: f64,
pub endpoint_threshold: f64,
pub resample_count: u32,
pub resample_spacing_meters: f64,
pub min_resample_points: u32,
pub max_resample_points: u32,
pub simplification_tolerance: f64,
pub max_simplified_points: u32,
}
impl Default for MatchConfig {
fn default() -> Self {
Self {
perfect_threshold: 30.0,
zero_threshold: 250.0,
min_match_percentage: 50.0,
min_route_distance: 500.0,
max_distance_diff_ratio: 0.30,
endpoint_threshold: 300.0,
resample_count: 50,
resample_spacing_meters: 50.0,
min_resample_points: 20,
max_resample_points: 200,
simplification_tolerance: 0.0001,
max_simplified_points: 100,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ffi", derive(uniffi::Record))]
pub struct RouteGroup {
pub group_id: String,
pub representative_id: String,
pub activity_ids: Vec<String>,
pub sport_type: String,
pub bounds: Option<Bounds>,
pub custom_name: Option<String>,
#[serde(default)]
pub best_time: Option<f64>,
#[serde(default)]
pub avg_time: Option<f64>,
#[serde(default)]
pub best_pace: Option<f64>,
#[serde(default)]
pub best_activity_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActivityMatchInfo {
pub activity_id: String,
pub match_percentage: f64,
pub direction: String,
}
#[derive(Debug, Clone)]
pub struct GroupingResult {
pub groups: Vec<RouteGroup>,
pub activity_matches: std::collections::HashMap<String, Vec<ActivityMatchInfo>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ffi", derive(uniffi::Record))]
pub struct ActivityMetrics {
pub activity_id: String,
pub name: String,
pub date: i64,
pub distance: f64,
pub moving_time: u32,
pub elapsed_time: u32,
pub elevation_gain: f64,
pub avg_hr: Option<u16>,
pub avg_power: Option<u16>,
pub sport_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ffi", derive(uniffi::Record))]
pub struct RoutePerformance {
pub activity_id: String,
pub name: String,
pub date: i64,
pub speed: f64,
pub duration: u32,
pub moving_time: u32,
pub distance: f64,
pub elevation_gain: f64,
pub avg_hr: Option<u16>,
pub avg_power: Option<u16>,
pub is_current: bool,
pub direction: String,
pub match_percentage: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ffi", derive(uniffi::Record))]
pub struct RoutePerformanceResult {
pub performances: Vec<RoutePerformance>,
pub best: Option<RoutePerformance>,
pub current_rank: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ffi", derive(uniffi::Record))]
pub struct SectionLap {
pub id: String,
#[serde(alias = "activity_id")]
pub activity_id: String,
pub time: f64,
pub pace: f64,
pub distance: f64,
pub direction: String,
#[serde(alias = "start_index")]
pub start_index: u32,
#[serde(alias = "end_index")]
pub end_index: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ffi", derive(uniffi::Record))]
pub struct SectionPerformanceRecord {
#[serde(alias = "activity_id")]
pub activity_id: String,
#[serde(alias = "activity_name")]
pub activity_name: String,
#[serde(alias = "activity_date")]
pub activity_date: i64,
pub laps: Vec<SectionLap>,
#[serde(alias = "lap_count")]
pub lap_count: u32,
#[serde(alias = "best_time")]
pub best_time: f64,
#[serde(alias = "best_pace")]
pub best_pace: f64,
#[serde(alias = "avg_time")]
pub avg_time: f64,
#[serde(alias = "avg_pace")]
pub avg_pace: f64,
pub direction: String,
#[serde(alias = "section_distance")]
pub section_distance: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ffi", derive(uniffi::Record))]
pub struct SectionPerformanceResult {
pub records: Vec<SectionPerformanceRecord>,
#[serde(alias = "best_record")]
pub best_record: Option<SectionPerformanceRecord>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ffi", derive(uniffi::Record))]
pub struct CustomSection {
pub id: String,
pub name: String,
pub polyline: Vec<GpsPoint>,
pub source_activity_id: String,
pub start_index: u32,
pub end_index: u32,
pub sport_type: String,
pub distance_meters: f64,
pub created_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ffi", derive(uniffi::Record))]
pub struct CustomSectionMatch {
pub activity_id: String,
pub start_index: u32,
pub end_index: u32,
pub direction: String,
pub distance_meters: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ffi", derive(uniffi::Record))]
pub struct CustomSectionMatchConfig {
pub proximity_threshold: f64,
pub min_coverage: f64,
}
impl Default for CustomSectionMatchConfig {
fn default() -> Self {
Self {
proximity_threshold: 50.0,
min_coverage: 0.8,
}
}
}
#[derive(Debug, Clone)]
pub struct RouteBounds {
pub activity_id: String,
pub min_lat: f64,
pub max_lat: f64,
pub min_lng: f64,
pub max_lng: f64,
pub distance: f64,
}
impl RTreeObject for RouteBounds {
type Envelope = AABB<[f64; 2]>;
fn envelope(&self) -> Self::Envelope {
AABB::from_corners([self.min_lng, self.min_lat], [self.max_lng, self.max_lat])
}
}
use crate::matching::calculate_route_distance;