mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 17:21:38 -05:00
Add documentation
This commit is contained in:
parent
f33dcdfe21
commit
a66f41d9fb
12 changed files with 202 additions and 65 deletions
|
|
@ -5,7 +5,6 @@ class Api::V1::TracksController < ApiController
|
|||
start_at = params[:start_at]&.to_datetime&.to_i
|
||||
end_at = params[:end_at]&.to_datetime&.to_i || Time.zone.now.to_i
|
||||
|
||||
# Find tracks that overlap with the time range
|
||||
tracks = current_api_user.tracks
|
||||
.where('start_at <= ? AND end_at >= ?', Time.zone.at(end_at), Time.zone.at(start_at))
|
||||
.order(start_at: :asc)
|
||||
|
|
@ -17,7 +16,6 @@ class Api::V1::TracksController < ApiController
|
|||
end
|
||||
|
||||
def create
|
||||
# Trigger track generation for the user
|
||||
Tracks::CreateJob.perform_later(current_api_user.id)
|
||||
|
||||
render json: { message: 'Track generation started' }
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ module Calculateable
|
|||
end
|
||||
|
||||
def convert_distance_for_storage(calculated_distance)
|
||||
# Store distance in user's preferred unit with 2 decimal places precision
|
||||
calculated_distance.round(2)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,6 @@ class Point < ApplicationRecord
|
|||
end
|
||||
|
||||
def country_name
|
||||
# Safely get country name from association or attribute
|
||||
self.country&.name || read_attribute(:country) || ''
|
||||
end
|
||||
|
||||
|
|
@ -103,11 +102,9 @@ class Point < ApplicationRecord
|
|||
end
|
||||
|
||||
def trigger_incremental_track_generation
|
||||
# Only trigger for recent points (within last day) to avoid processing old data
|
||||
point_date = Time.zone.at(timestamp).to_date
|
||||
return unless point_date >= 1.day.ago.to_date
|
||||
|
||||
# Schedule incremental track generation for this user and day
|
||||
IncrementalTrackGeneratorJob.perform_later(user_id, point_date.to_s, 5)
|
||||
Tracks::IncrementalGeneratorJob.perform_later(user_id, point_date.to_s, 5)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,10 +14,6 @@ class Track < ApplicationRecord
|
|||
after_update :broadcast_track_updated
|
||||
after_destroy :broadcast_track_destroyed
|
||||
|
||||
# Find the last track for a user on a specific day
|
||||
# @param user [User] the user to find tracks for
|
||||
# @param day [Date, Time] the day to search for tracks
|
||||
# @return [Track, nil] the last track for that day or nil if none found
|
||||
def self.last_for_day(user, day)
|
||||
day_start = day.beginning_of_day
|
||||
day_end = day.end_of_day
|
||||
|
|
|
|||
|
|
@ -9,43 +9,30 @@ class TrackSerializer
|
|||
def call
|
||||
return [] if track_ids.empty?
|
||||
|
||||
# Show only tracks that have points in the selected timeframe
|
||||
tracks_data = user.tracks
|
||||
tracks = user.tracks
|
||||
.where(id: track_ids)
|
||||
.order(start_at: :asc)
|
||||
.pluck(:id, :start_at, :end_at, :distance, :avg_speed, :duration,
|
||||
:elevation_gain, :elevation_loss, :elevation_max, :elevation_min, :original_path)
|
||||
|
||||
tracks_data.map do |id, start_at, end_at, distance, avg_speed, duration,
|
||||
elevation_gain, elevation_loss, elevation_max, elevation_min, original_path|
|
||||
serialize_track_data(
|
||||
id, start_at, end_at, distance, avg_speed, duration, elevation_gain,
|
||||
elevation_loss, elevation_max, elevation_min, original_path
|
||||
)
|
||||
end
|
||||
tracks.map { |track| serialize_track_data(track) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :user, :track_ids
|
||||
|
||||
def serialize_track_data(
|
||||
id, start_at, end_at, distance, avg_speed, duration, elevation_gain,
|
||||
elevation_loss, elevation_max, elevation_min, original_path
|
||||
)
|
||||
|
||||
def serialize_track_data(track)
|
||||
{
|
||||
id: id,
|
||||
start_at: start_at.iso8601,
|
||||
end_at: end_at.iso8601,
|
||||
distance: distance.to_i,
|
||||
avg_speed: avg_speed.to_f,
|
||||
duration: duration,
|
||||
elevation_gain: elevation_gain,
|
||||
elevation_loss: elevation_loss,
|
||||
elevation_max: elevation_max,
|
||||
elevation_min: elevation_min,
|
||||
original_path: original_path.to_s
|
||||
id: track.id,
|
||||
start_at: track.start_at.iso8601,
|
||||
end_at: track.end_at.iso8601,
|
||||
distance: track.distance.to_i,
|
||||
avg_speed: track.avg_speed.to_f,
|
||||
duration: track.duration,
|
||||
elevation_gain: track.elevation_gain,
|
||||
elevation_loss: track.elevation_loss,
|
||||
elevation_max: track.elevation_max,
|
||||
elevation_min: track.elevation_min,
|
||||
original_path: track.original_path.to_s
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ class OwnTracks::Params
|
|||
attr_reader :params
|
||||
|
||||
def initialize(params)
|
||||
@params = params.deep_symbolize_keys
|
||||
@params = params.to_h.deep_symbolize_keys
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
|
|
|
|||
|
|
@ -1,5 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# The core track generation engine that orchestrates the entire process of creating tracks from GPS points.
|
||||
#
|
||||
# This class uses a flexible strategy pattern to handle different track generation scenarios:
|
||||
# - Bulk processing: Generate all tracks at once from existing points
|
||||
# - Incremental processing: Generate tracks as new points arrive
|
||||
#
|
||||
# How it works:
|
||||
# 1. Uses a PointLoader strategy to load points from the database
|
||||
# 2. Applies segmentation logic to split points into track segments based on time/distance gaps
|
||||
# 3. Determines which segments should be finalized into tracks vs buffered for later
|
||||
# 4. Creates Track records from finalized segments with calculated statistics
|
||||
# 5. Manages cleanup of existing tracks based on the chosen strategy
|
||||
#
|
||||
# Strategy Components:
|
||||
# - point_loader: Loads points from database (BulkLoader, IncrementalLoader)
|
||||
# - incomplete_segment_handler: Handles segments that aren't ready to finalize (IgnoreHandler, BufferHandler)
|
||||
# - track_cleaner: Manages existing tracks when regenerating (ReplaceCleaner, NoOpCleaner)
|
||||
#
|
||||
# The class includes Tracks::Segmentation for splitting logic and Tracks::TrackBuilder for track creation.
|
||||
# Distance and time thresholds are configurable per user via their settings.
|
||||
#
|
||||
# Example usage:
|
||||
# generator = Tracks::Generator.new(
|
||||
# user,
|
||||
# point_loader: Tracks::PointLoaders::BulkLoader.new(user),
|
||||
# incomplete_segment_handler: Tracks::IncompleteSegmentHandlers::IgnoreHandler.new(user),
|
||||
# track_cleaner: Tracks::TrackCleaners::ReplaceCleaner.new(user)
|
||||
# )
|
||||
# tracks_created = generator.call
|
||||
#
|
||||
module Tracks
|
||||
class Generator
|
||||
include Tracks::Segmentation
|
||||
|
|
|
|||
|
|
@ -1,5 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Incomplete segment handling strategy for bulk track generation.
|
||||
#
|
||||
# This handler always finalizes segments immediately without buffering,
|
||||
# making it suitable for bulk processing where all data is historical
|
||||
# and no segments are expected to grow with new incoming points.
|
||||
#
|
||||
# How it works:
|
||||
# 1. Always returns true for should_finalize_segment? - every segment becomes a track
|
||||
# 2. Ignores any incomplete segments (logs them but takes no action)
|
||||
# 3. Requires no cleanup since no data is buffered
|
||||
#
|
||||
# Used primarily for:
|
||||
# - Bulk track generation from historical data
|
||||
# - One-time processing where all points are already available
|
||||
# - Scenarios where you want to create tracks from every valid segment
|
||||
#
|
||||
# This strategy is efficient for bulk operations but not suitable for
|
||||
# real-time processing where segments may grow as new points arrive.
|
||||
#
|
||||
# Example usage:
|
||||
# handler = Tracks::IncompleteSegmentHandlers::IgnoreHandler.new(user)
|
||||
# should_create_track = handler.should_finalize_segment?(segment_points)
|
||||
#
|
||||
module Tracks
|
||||
module IncompleteSegmentHandlers
|
||||
class IgnoreHandler
|
||||
|
|
|
|||
|
|
@ -1,5 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Point loading strategy for bulk track generation from existing GPS points.
|
||||
#
|
||||
# This loader retrieves all valid points for a user within an optional time range,
|
||||
# suitable for regenerating all tracks at once or processing historical data.
|
||||
#
|
||||
# How it works:
|
||||
# 1. Queries all points belonging to the user
|
||||
# 2. Filters out points without valid coordinates or timestamps
|
||||
# 3. Optionally filters by start_at/end_at time range if provided
|
||||
# 4. Returns points ordered by timestamp for sequential processing
|
||||
#
|
||||
# Used primarily for:
|
||||
# - Initial track generation when a user first enables tracks
|
||||
# - Bulk regeneration of all tracks after settings changes
|
||||
# - Processing historical data imports
|
||||
#
|
||||
# The loader is designed to be efficient for large datasets while ensuring
|
||||
# data integrity by filtering out invalid points upfront.
|
||||
#
|
||||
# Example usage:
|
||||
# loader = Tracks::PointLoaders::BulkLoader.new(user, start_at: 1.week.ago, end_at: Time.current)
|
||||
# points = loader.load_points
|
||||
#
|
||||
module Tracks
|
||||
module PointLoaders
|
||||
class BulkLoader
|
||||
|
|
|
|||
|
|
@ -1,5 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Track segmentation logic for splitting GPS points into meaningful track segments.
|
||||
#
|
||||
# This module provides the core algorithm for determining where one track ends
|
||||
# and another begins, based on time gaps and distance jumps between consecutive points.
|
||||
#
|
||||
# How it works:
|
||||
# 1. Analyzes consecutive GPS points to detect gaps that indicate separate journeys
|
||||
# 2. Uses configurable time and distance thresholds to identify segment boundaries
|
||||
# 3. Splits large arrays of points into smaller arrays representing individual tracks
|
||||
# 4. Provides utilities for handling both Point objects and hash representations
|
||||
#
|
||||
# Segmentation criteria:
|
||||
# - Time threshold: Gap longer than X minutes indicates a new track
|
||||
# - Distance threshold: Jump larger than X meters indicates a new track
|
||||
# - Minimum segment size: Segments must have at least 2 points to form a track
|
||||
#
|
||||
# The module is designed to be included in classes that need segmentation logic
|
||||
# and requires the including class to implement distance_threshold_meters and
|
||||
# time_threshold_minutes methods.
|
||||
#
|
||||
# Used by:
|
||||
# - Tracks::Generator for splitting points during track generation
|
||||
# - Tracks::CreateFromPoints for legacy compatibility
|
||||
#
|
||||
# Example usage:
|
||||
# class MyTrackProcessor
|
||||
# include Tracks::Segmentation
|
||||
#
|
||||
# def distance_threshold_meters; 500; end
|
||||
# def time_threshold_minutes; 60; end
|
||||
#
|
||||
# def process_points(points)
|
||||
# segments = split_points_into_segments(points)
|
||||
# # Process each segment...
|
||||
# end
|
||||
# end
|
||||
#
|
||||
module Tracks::Segmentation
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,54 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Track creation and statistics calculation module for building Track records from GPS points.
|
||||
#
|
||||
# This module provides the core functionality for converting arrays of GPS points into
|
||||
# Track database records with calculated statistics including distance, duration, speed,
|
||||
# and elevation metrics.
|
||||
#
|
||||
# How it works:
|
||||
# 1. Takes an array of Point objects representing a track segment
|
||||
# 2. Creates a Track record with basic temporal and spatial boundaries
|
||||
# 3. Calculates comprehensive statistics: distance, duration, average speed
|
||||
# 4. Computes elevation metrics: gain, loss, maximum, minimum
|
||||
# 5. Builds a LineString path representation for mapping
|
||||
# 6. Associates all points with the created track
|
||||
#
|
||||
# Statistics calculated:
|
||||
# - Distance: In user's preferred unit (km/miles) with 2 decimal precision
|
||||
# - Duration: Total time in seconds between first and last point
|
||||
# - Average speed: In km/h regardless of user's distance unit preference
|
||||
# - Elevation gain/loss: Cumulative ascent and descent in meters
|
||||
# - Elevation max/min: Highest and lowest altitudes in the track
|
||||
#
|
||||
# The module respects user preferences for distance units and handles missing
|
||||
# elevation data gracefully by providing default values.
|
||||
#
|
||||
# Used by:
|
||||
# - Tracks::Generator for creating tracks during generation
|
||||
# - Any class that needs to convert point arrays to Track records
|
||||
#
|
||||
# Example usage:
|
||||
# class MyTrackProcessor
|
||||
# include Tracks::TrackBuilder
|
||||
#
|
||||
# def initialize(user)
|
||||
# @user = user
|
||||
# end
|
||||
#
|
||||
# def process_segment(points)
|
||||
# track = create_track_from_points(points)
|
||||
# # Track now exists with calculated statistics
|
||||
# end
|
||||
#
|
||||
# private
|
||||
#
|
||||
# attr_reader :user
|
||||
# end
|
||||
#
|
||||
module Tracks::TrackBuilder
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
# Create a track from an array of points
|
||||
# @param points [Array<Point>] array of Point objects
|
||||
# @return [Track, nil] created track or nil if creation failed
|
||||
def create_track_from_points(points)
|
||||
return nil if points.size < 2
|
||||
|
||||
|
|
@ -37,38 +80,25 @@ module Tracks::TrackBuilder
|
|||
end
|
||||
end
|
||||
|
||||
# Build path from points using existing BuildPath service
|
||||
# @param points [Array<Point>] array of Point objects
|
||||
# @return [String] LineString representation of the path
|
||||
def build_path(points)
|
||||
Tracks::BuildPath.new(points.map(&:lonlat)).call
|
||||
end
|
||||
|
||||
# Calculate track distance in user's preferred unit for storage
|
||||
# @param points [Array<Point>] array of Point objects
|
||||
# @return [Float] distance in user's preferred unit with 2 decimal places precision
|
||||
def calculate_track_distance(points)
|
||||
distance_in_user_unit = Point.total_distance(points, user.safe_settings.distance_unit || 'km')
|
||||
distance_in_user_unit.round(2)
|
||||
end
|
||||
|
||||
# Calculate track duration in seconds
|
||||
# @param points [Array<Point>] array of Point objects
|
||||
# @return [Integer] duration in seconds
|
||||
def calculate_duration(points)
|
||||
points.last.timestamp - points.first.timestamp
|
||||
end
|
||||
|
||||
# Calculate average speed in km/h
|
||||
# @param distance_in_user_unit [Numeric] distance in user's preferred unit
|
||||
# @param duration_seconds [Numeric] duration in seconds
|
||||
# @return [Float] average speed in km/h
|
||||
def calculate_average_speed(distance_in_user_unit, duration_seconds)
|
||||
return 0.0 if duration_seconds <= 0 || distance_in_user_unit <= 0
|
||||
|
||||
# Convert distance to meters for speed calculation
|
||||
distance_meters = case user.safe_settings.distance_unit
|
||||
when 'miles', 'mi'
|
||||
when 'mi'
|
||||
distance_in_user_unit * 1609.344 # miles to meters
|
||||
else
|
||||
distance_in_user_unit * 1000 # km to meters
|
||||
|
|
@ -79,9 +109,6 @@ module Tracks::TrackBuilder
|
|||
(speed_mps * 3.6).round(2) # m/s to km/h
|
||||
end
|
||||
|
||||
# Calculate elevation statistics from points
|
||||
# @param points [Array<Point>] array of Point objects
|
||||
# @return [Hash] elevation statistics hash
|
||||
def calculate_elevation_stats(points)
|
||||
altitudes = points.map(&:altitude).compact
|
||||
|
||||
|
|
@ -109,8 +136,6 @@ module Tracks::TrackBuilder
|
|||
}
|
||||
end
|
||||
|
||||
# Default elevation statistics when no altitude data is available
|
||||
# @return [Hash] default elevation statistics
|
||||
def default_elevation_stats
|
||||
{
|
||||
gain: 0,
|
||||
|
|
@ -122,8 +147,6 @@ module Tracks::TrackBuilder
|
|||
|
||||
private
|
||||
|
||||
# This method must be implemented by the including class
|
||||
# @return [User] the user for which tracks are being created
|
||||
def user
|
||||
raise NotImplementedError, "Including class must implement user method"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,5 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Track cleaning strategy for bulk track regeneration.
|
||||
#
|
||||
# This cleaner removes existing tracks before generating new ones,
|
||||
# ensuring a clean slate for bulk processing without duplicate tracks.
|
||||
#
|
||||
# How it works:
|
||||
# 1. Finds all existing tracks for the user within the specified time range
|
||||
# 2. Detaches all points from these tracks (sets track_id to nil)
|
||||
# 3. Destroys the existing track records
|
||||
# 4. Allows the generator to create fresh tracks from the same points
|
||||
#
|
||||
# Used primarily for:
|
||||
# - Bulk track regeneration after settings changes
|
||||
# - Reprocessing historical data with updated algorithms
|
||||
# - Ensuring consistency when tracks need to be rebuilt
|
||||
#
|
||||
# The cleaner respects optional time boundaries (start_at/end_at) to enable
|
||||
# partial regeneration of tracks within specific time windows.
|
||||
#
|
||||
# This strategy is essential for bulk operations but should not be used
|
||||
# for incremental processing where existing tracks should be preserved.
|
||||
#
|
||||
# Example usage:
|
||||
# cleaner = Tracks::TrackCleaners::ReplaceCleaner.new(user, start_at: 1.week.ago, end_at: Time.current)
|
||||
# cleaner.cleanup_if_needed
|
||||
#
|
||||
module Tracks
|
||||
module TrackCleaners
|
||||
class ReplaceCleaner
|
||||
|
|
@ -17,10 +43,8 @@ module Tracks
|
|||
if tracks_to_remove.any?
|
||||
Rails.logger.info "Removing #{tracks_to_remove.count} existing tracks for user #{user.id}"
|
||||
|
||||
# Set track_id to nil for all points in these tracks
|
||||
Point.where(track_id: tracks_to_remove.ids).update_all(track_id: nil)
|
||||
|
||||
# Remove the tracks
|
||||
tracks_to_remove.destroy_all
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in a new issue