Skip to content

Commit

Permalink
Merge pull request #31 from dantleech/ranking
Browse files Browse the repository at this point in the history
Add Support for Ranking
  • Loading branch information
dantleech committed Sep 3, 2023
2 parents 3c31c64 + 92c74a8 commit 218d185
Show file tree
Hide file tree
Showing 12 changed files with 250 additions and 140 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ CHANGELOG

## master

Features:

- Highlight and allow selection of current split in activity view.
- Added activity ranking.
82 changes: 26 additions & 56 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ use std::{
time::{Duration, SystemTime},
};

use strum::EnumIter;

use tokio::sync::mpsc::{Receiver, Sender};
use tui::{
backend::{Backend, CrosstermBackend},
Expand All @@ -18,7 +16,7 @@ use crate::{
component::activity_list::{ActivityListMode, ActivityListState, ActivityViewState},
event::{input::EventSender, util::{table_state_prev, table_state_next}},
input::InputEvent,
store::{activity::{ActivityStore, Activities}},
store::{activity::{ActivityStore, Activities, SortBy, SortOrder}},
};
use crate::{
component::{activity_list, activity_view, unit_formatter::UnitFormatter},
Expand All @@ -34,6 +32,11 @@ pub struct ActivityFilters {
pub filter: String,
}

pub struct RankOptions {
pub rank_by: SortBy,
pub rank_order: SortOrder,
}

impl ActivityFilters {
pub fn anchor_tolerance_add(&mut self, delta: f64) {
self.anchor_tolerance += delta;
Expand Down Expand Up @@ -73,12 +76,12 @@ pub struct App<'a> {
pub activity_list: ActivityListState,
pub activity_view: ActivityViewState,
pub filters: ActivityFilters,
pub ranking: RankOptions,

pub activity_type: Option<String>,
pub activity: Option<Activity>,
pub activity_anchored: Option<Activity>,
pub activities: Activities,
pub activities_filtered: Activities,

pub info_message: Option<Notification>,
pub error_message: Option<Notification>,
Expand All @@ -96,39 +99,6 @@ pub enum ActivePage {
Activity,
}

#[derive(EnumIter)]
pub enum SortBy {
Date,
Distance,
Pace,
HeartRate,
Time,
}

impl Display for SortBy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_label())
}
}

pub enum SortOrder {
Asc,
Desc,
}

impl Display for SortOrder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
SortOrder::Asc => "ascending",
SortOrder::Desc => "descending",
}
)
}
}

impl App<'_> {
pub fn new<'a>(
store: &'a mut ActivityStore<'a>,
Expand All @@ -147,6 +117,7 @@ impl App<'_> {
filter_text_area: Input::default(),
filter_dialog: false,
sort_dialog: false,
rank_dialog: false,
},
activity_view: ActivityViewState {
pace_table_state: TableState::default(),
Expand All @@ -158,10 +129,13 @@ impl App<'_> {
filter: "".to_string(),
anchor_tolerance: 0.005,
},
ranking: RankOptions {
rank_by: SortBy::Pace,
rank_order: SortOrder::Desc,
},
activity: None,
activity_anchored: None,
activities: Activities::new(),
activities_filtered: Activities::new(),
store,

activity_type: None,
Expand All @@ -177,8 +151,6 @@ impl App<'_> {
&mut self,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) -> Result<(), anyhow::Error> {
self.activities = self.store.activities().await;

loop {
if self.quit {
break;
Expand Down Expand Up @@ -226,23 +198,21 @@ impl App<'_> {
}

pub async fn reload(&mut self) {
self.activities = self.store.activities().await;
self.activities_filtered = self.activities.where_title_contains(self.filters.filter.as_str());
let mut activities = self.store.activities().await;
activities = activities.where_title_contains(self.filters.filter.as_str());
if let Some(activity_type) = self.activity_type.clone() {
self.activities_filtered = self.activities_filtered.having_activity_type(activity_type);
activities = activities.having_activity_type(activity_type);
}
if let Some(anchored) = &self.activity_anchored {
self.activities_filtered = self.activities_filtered.withing_distance_of(anchored, self.filters.anchor_tolerance);
activities = activities.withing_distance_of(anchored, self.filters.anchor_tolerance);
}
self.activities = activities
.rank(&self.ranking.rank_by, &self.ranking.rank_order)
.sort(&self.filters.sort_by, &self.filters.sort_order)
}

pub fn unsorted_filtered_activities(&self) -> Activities {
self.activities_filtered.clone()
}

pub fn filtered_activities(&self) -> Activities {
let activities = self.unsorted_filtered_activities();
activities.sort(&self.filters.sort_by, &self.filters.sort_order)
pub fn activities(&self) -> Activities {
self.activities.clone()
}

fn draw<B: Backend>(&mut self, f: &mut Frame<B>) -> Result<(), anyhow::Error> {
Expand All @@ -261,7 +231,7 @@ impl App<'_> {
}

pub(crate) fn anchor_selected(&mut self) {
let activities = self.filtered_activities();
let activities = self.activities();
if let Some(selected) = self.activity_list.table_state().selected() {
if let Some(a) = activities.get(selected) {
if self.activity_anchored.is_some() {
Expand All @@ -280,11 +250,11 @@ impl App<'_> {
pub(crate) fn previous_activity(&mut self) {
table_state_prev(
self.activity_list.table_state(),
self.activities_filtered.len(),
self.activities.len(),
false,
);
if let Some(selected) = self.activity_list.table_state().selected() {
if let Some(a) = self.activities_filtered.get(selected) {
if let Some(a) = self.activities.get(selected) {
self.activity = Some(a.clone());
}
}
Expand All @@ -293,11 +263,11 @@ impl App<'_> {
pub(crate) fn next_activity(&mut self) {
table_state_next(
self.activity_list.table_state(),
self.activities_filtered.len(),
self.activities.len(),
false,
);
if let Some(selected) = self.activity_list.table_state().selected() {
if let Some(a) = self.activities_filtered.get(selected) {
if let Some(a) = self.activities.get(selected) {
self.activity = Some(a.clone());
}
}
Expand Down
39 changes: 21 additions & 18 deletions src/component/activity_list/chart.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use chrono::{NaiveDateTime};
use chrono::NaiveDateTime;
use tui::{
backend::Backend,
layout::Constraint,
Expand All @@ -10,21 +10,20 @@ use tui::{
};

use crate::{
app::{App},
event::{
keymap::{MappedKey},
},
app::App,
event::keymap::MappedKey,
store::activity::{SortBy, SortOrder},
};



pub fn handle(_app: &mut App, _key: MappedKey) {}
pub fn draw<B: Backend>(
app: &mut App,
f: &mut Frame<B>,
area: tui::layout::Rect,
) -> Result<(), anyhow::Error> {
let activities = &app.unsorted_filtered_activities();
let activities = &app
.activities()
.sort(&SortBy::Date, &SortOrder::Asc);
let times: Vec<i64> = activities.timestamps();
let paces: Vec<i64> = activities.meter_per_hours();
let tmax = times.iter().max();
Expand All @@ -41,7 +40,8 @@ pub fn draw<B: Backend>(
}
let pmin = pmin.unwrap();
let pmax = pmax.unwrap();
let data: Vec<(f64, f64)> = activities.to_vec()
let data: Vec<(f64, f64)> = activities
.to_vec()
.iter()
.map(|a| {
let ts = a.start_date.unwrap().timestamp();
Expand All @@ -50,11 +50,11 @@ pub fn draw<B: Backend>(
.collect();
let mut current = vec![];
if let Some(selected) = app.activity_list.table_state().selected() {
let activities = app.filtered_activities();
let activities = app.activities();
if let Some(a) = activities.get(selected) {
if let Some(a) = app.activities.find(a.id) {
current.push((a.start_date.unwrap().timestamp() as f64, *pmin as f64));
current.push((a.start_date.unwrap().timestamp() as f64, *pmax as f64));
if let Some(a) = activities.find(a.id) {
current.push((a.start_date.unwrap().timestamp() as f64, *pmin as f64));
current.push((a.start_date.unwrap().timestamp() as f64, *pmax as f64));
}
}
}
Expand All @@ -71,17 +71,17 @@ pub fn draw<B: Backend>(
.marker(Marker::Braille)
.graph_type(GraphType::Scatter)
.style(Style::default().fg(Color::Magenta)),
Dataset::default().data(&current)
Dataset::default()
.data(&current)
.name("Selected")
.marker(Marker::Braille)
.graph_type(GraphType::Line)
.style(Style::default().fg(Color::Green)),
];
let yaxisstep = (pdiff as f64 / area.height as f64) as usize;
let yaxis =
(*pmin..*pmax).step_by(if yaxisstep > 0 { yaxisstep } else { 1 });
let yaxis = (*pmin..*pmax).step_by(if yaxisstep > 0 { yaxisstep } else { 1 });
let xaxisstep = (tdiff as f64 / 5.0) as usize;
let xaxis = (*tmin.unwrap()..*tmax.unwrap()).step_by(if xaxisstep > 0 { xaxisstep } else {1});
let xaxis = (*tmin.unwrap()..*tmax.unwrap()).step_by(if xaxisstep > 0 { xaxisstep } else { 1 });
let chart = Chart::new(datasets)
.hidden_legend_constraints((Constraint::Max(1), Constraint::Max(1)))
.block(Block::default().borders(Borders::all()))
Expand All @@ -105,7 +105,10 @@ pub fn draw<B: Backend>(
Axis::default()
.title(Span::styled("Pace", Style::default().fg(Color::Red)))
.style(Style::default().fg(Color::White))
.bounds([*pmin as f64, *pmax as f64 + (pdiff as f64 / activities.len() as f64)])
.bounds([
*pmin as f64,
*pmax as f64 + (pdiff as f64 / activities.len() as f64),
])
.labels(
yaxis
.map(|p| Span::from(app.unit_formatter.pace(3600, p as f64)))
Expand Down
30 changes: 24 additions & 6 deletions src/component/activity_list/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ use tui::{
use tui_input::backend::crossterm::EventHandler;

use crate::{
app::{App, SortOrder},
app::App,
event::{
keymap::{MappedKey, StravaEvent}, input::InputEvent,
},
store::activity::{Activities},
store::activity::{Activities, SortOrder},
ui::{centered_rect_absolute, color::ColorTheme}, component::{table_status_select_current},
};

use super::sort_dialog;
use super::{sort_dialog, rank_dialog};

pub fn handle(app: &mut App, key: MappedKey) {
if app.activity_list.filter_dialog {
Expand Down Expand Up @@ -46,6 +46,11 @@ pub fn handle(app: &mut App, key: MappedKey) {

return;
}
if app.activity_list.rank_dialog {
rank_dialog::handle(app, key);

return;
}
match key.strava_event {
StravaEvent::Quit => app.quit = true,
StravaEvent::ToggleUnitSystem => {
Expand All @@ -55,12 +60,14 @@ pub fn handle(app: &mut App, key: MappedKey) {
app.filters.sort_order = match app.filters.sort_order {
SortOrder::Asc => SortOrder::Desc,
SortOrder::Desc => SortOrder::Asc,
}
};
app.send(InputEvent::Reload);
}
StravaEvent::Down => app.next_activity(),
StravaEvent::Up => app.previous_activity(),
StravaEvent::Filter => toggle_filter(app),
StravaEvent::Sort => toggle_sort(app),
StravaEvent::Rank => toggle_rank(app),
StravaEvent::Enter => table_status_select_current(app),
StravaEvent::Refresh => app.send(InputEvent::Sync),
StravaEvent::IncreaseTolerance => {
Expand All @@ -86,13 +93,16 @@ fn toggle_filter(app: &mut App) {
fn toggle_sort(app: &mut App) {
app.activity_list.sort_dialog = !app.activity_list.sort_dialog;
}
fn toggle_rank(app: &mut App) {
app.activity_list.rank_dialog = !app.activity_list.rank_dialog;
}

pub fn draw<B: Backend>(
app: &mut App,
f: &mut Frame<B>,
area: tui::layout::Rect,
) -> Result<(), anyhow::Error> {
let activities = &app.filtered_activities();
let activities = &app.activities();

if app.activity_list.table_state().selected().is_none() && !activities.is_empty() {
app.activity_list.table_state().select(Some(0));
Expand Down Expand Up @@ -129,6 +139,11 @@ pub fn draw<B: Backend>(

return Ok(());
}
if app.activity_list.rank_dialog {
rank_dialog::draw(app, f, f.size())?;

return Ok(());
}

Ok(())
}
Expand All @@ -142,8 +157,9 @@ pub fn activity_list_table<'a>(app: &App, activities: &'a Activities) -> Table<'
"Dst",
"πŸ•‘ Time",
"πŸ‘£ Pace",
"πŸ’“ Heart",
"πŸ’“ Avg. Heart",
"πŸŒ„ Elevation",
"πŸͺœ Rank",
];
let headers = header_names
.iter()
Expand All @@ -169,6 +185,7 @@ pub fn activity_list_table<'a>(app: &App, activities: &'a Activities) -> Table<'
.map_or_else(|| "n/a".to_string(), |v| format!("{:.2}", v)),
),
Cell::from(app.unit_formatter.elevation(activity.total_elevation_gain)),
Cell::from(format!("{}", activity.rank)),
]));
}

Expand All @@ -190,6 +207,7 @@ pub fn activity_list_table<'a>(app: &App, activities: &'a Activities) -> Table<'
Constraint::Percentage(10),
Constraint::Percentage(10),
Constraint::Percentage(10),
Constraint::Percentage(10),
])
}

0 comments on commit 218d185

Please sign in to comment.