When designing an activity tracking system, it’s essential to think ahead and plan for flexibility. The system should be able to integrate with different providers, such as Strava, and adapt to new providers in the future. To achieve this flexibility, I introduce a new entity called “Activity.” This entity will serve as a central point for storing activity data, allowing us to switch providers seamlessly.
To create the “Activity” entity, I start by defining its structure in the database. Here’s the migration to create the “activities” table:
class CreateActivities < ActiveRecord::Migration[7.0]
def change
create_table :activities do |t|
t.float "distance"
t.integer "time"
t.string "external_id"
t.datetime "activity_date"
t.string "name"
t.timestamps
end
end
end
I also define the “Activity” record and its associated repository to interact with the database.
In my example, I focus on integrating with Strava, but keep in mind that the system’s design allows for easy integration with other providers. I use the Strava API to fetch activities, update access tokens, and store new activities. The “sync_activities” endpoint handles this integration.
Activities can vary, and they may be associated with different types of bikes and components. Strava provides data that helps us categorize activities into sport types and mark them as commutes. To add flexibility to my system, I introduce two new fields in the “bikes” table: “sport_type” and “commute.”
To maintain consistency, I update the “Bike” model and add default values for these fields in a new migration:
class AddDefaultTypesToBikes < ActiveRecord::Migration[7.0]
def change
add_column :bikes, :sport_type, :string
add_column :bikes, :commute, :boolean, default: false
end
end
Now I’m able to save activities. In previous post I get activities from external API. Now I’m able to change it and not only fetch data, but also save the data.
module App
class SyncStravaActivities
def initialize
@strava_oauth_client = ::Strava::OAuth::Client.new(
client_id: ENV["STRAVA_CLIENT_ID"],
client_secret: ENV["STRAVA_CLIENT_SECRET"]
)
end
def call
update_tokens
sync_all_activities
end
private
def update_tokens
response = @strava_oauth_client.oauth_token(
refresh_token: Repositories::StravaIntegrations.new.get_refresh_token,
grant_type: "refresh_token"
)
Repositories::StravaIntegrations.new.update_credentials(
access_token: response.access_token, refresh_token: response.refresh_token
)
end
def sync_all_activities
client = Strava::Api::Client.new(access_token: Repositories::StravaIntegrations.new.get_access_token)
page = 1
loop do
activities = client.athlete_activities(page: page)
activities.each do |activity|
next if Repositories::Activities.new.find_by_external_id(activity["id"])
Repositories::Activities.new.create(
distance: activity["distance"],
time: activity["moving_time"],
external_id: activity["id"],
activity_date: activity["start_date"],
name: activity["name"],
commute: activity["commute"],
sport_type: activity["sport_type"]
)
end
break if activities.empty?
page += 1
end
end
end
end
Now comes the challenging part: calculating distances and times for bikes and components. Since components can have multiple assignments to bikes, I need to consider time slots. Here’s a simplified approach to calculate distances and times:
Title: Building a Flexible Bike Activity Tracking System
In the world of fitness and outdoor activities, there’s a growing need for flexible systems that can track various types of activities, whether it’s a bike ride, a virtual ride, or a daily commute. In this blog post, I’ll explore the development of a flexible activity tracking system that can integrate with different providers, store activity data, and calculate distances and times for bikes and components.
The Need for Flexibility When designing an activity tracking system, it’s essential to think ahead and plan for flexibility. The system should be able to integrate with different providers, such as Strava, and adapt to new providers in the future. To achieve this flexibility, I introduce a new entity called “Activity.” This entity will serve as a central point for storing activity data, allowing us to switch providers seamlessly.
The Entity: Activity To create the “Activity” entity, I start by defining its structure in the database. Here’s the migration to create the “activities” table:
class CreateActivities < ActiveRecord::Migration[7.0]
def change
create_table :activities do |t|
t.float "distance"
t.integer "time"
t.string "external_id"
t.datetime "activity_date"
t.string "name"
t.timestamps
end
end
end
I also define the “Activity” model and its associated repository to interact with the database.
Integration with Strava In our example, I focus on integrating with Strava, but keep in mind that the system’s design allows for easy integration with other providers. I use the Strava API to fetch activities, update access tokens, and store new activities. The “sync_activities” endpoint handles this integration.
Adding Flexibility to Activity Types Activities can vary, and they may be associated with different types of bikes and components. Strava provides data that helps us categorize activities into sport types and mark them as commutes. To add flexibility to our system, I introduce two new fields in the “bikes” table: “sport_type” and “commute.”
To maintain consistency, I update the “Bike” model and add default values for these fields in a new migration:
class AddDefaultTypesToBikes < ActiveRecord::Migration[7.0]
def change
add_column :bikes, :sport_type, :string
add_column :bikes, :commute, :boolean, default: false
end
end
Now comes the challenging part: calculating distances and times for bikes and components. Since components can have multiple assignments to bikes, I need to consider time slots. Here’s a simplified approach to calculate distances and times:
def calculate_distance(record)
total_distance = 0
record.component_assignments.each do |ca|
activity_query = Records::Activity.where(commute: ca.bike.commute, sport_type: ca.bike.sport_type)
activity_query = activity_query.where("activity_date >= ?", ca.started_at)
activity_query = activity_query.where("activity_date <= ?", ca.ended_at) if ca.ended_at
total_distance += activity_query.sum(:distance)
end
(total_distance / 1000).round(2).to_s + " KM"
end
def calculate_time(record)
total_time = 0
record.component_assignments each do |ca|
activity_query = Records::Activity.where(commute: ca.bike.commute, sport_type: ca.bike.sport_type)
activity_query = activity_query.where("activity_date >= ?", ca.started_at)
activity_query = activity_query.where("activity_date <= ?", ca.ended_at) if ca.ended_at
total_time += activity_query.sum(:time)
end
humanize(total_time)
end
This code loops through component assignments, considers time slots, and calculates the total distance and time for the given bike or component. The same code but for bikes is little simpler:
def calculate_distance(record)
(Records::Activity.where(commute: record.commute, sport_type: record.sport_type).sum(:distance) / 1000).round(2).to_s + " KM"
end
def calculate_time(record)
seconds = Records::Activity.where(commute: record.commute, sport_type: record.sport_type).sum(:time)
humanize(seconds)
end
In both cases there is method humanize
which convert time to more readable form:
def humanize(secs)
[[60, :seconds], [60, :minutes], [Float::INFINITY, :hours]].map { |count, name|
if secs > 0
secs, n = secs.divmod(count)
"#{n.to_i} #{name}" unless n.to_i == 0
end
}.compact.reverse.join(" ")
end
While this system works, there’s room for improvement. For example, I could implement caching to make distance and time calculations more efficient, especially if the system deals with a large volume of data.
In conclusion, building a flexible activity tracking system that can adapt to different providers and handle various activity types is a complex but rewarding endeavor. By focusing on flexibility, data consistency, and efficient calculations, I can create a robust system that meets the evolving needs of my users.